diff --git a/Parsing/AST.cs b/Parsing/AST.cs index a5dac16c..6a115ded 100644 --- a/Parsing/AST.cs +++ b/Parsing/AST.cs @@ -440,7 +440,9 @@ public record While(Expr Condition, Stmt Body) : Stmt; public record For(Stmt? Initializer, Expr? Condition, Expr? Increment, Stmt Body) : Stmt; public record DoWhile(Stmt Body, Expr Condition) : Stmt; public record ForOf(Token Variable, string? TypeAnnotation, Expr Iterable, Stmt Body, bool IsAsync = false) : Stmt; - public record ForIn(Token Variable, string? TypeAnnotation, Expr Object, Stmt Body) : Stmt; + // IsDeclaration distinguishes `for (var x in o)` (a fresh binding) from `for (x in o)` + // (assignment to an existing lvalue) — the latter's target type is validated (TS2405). + public record ForIn(Token Variable, string? TypeAnnotation, Expr Object, Stmt Body, bool IsDeclaration = true) : Stmt; public record If(Expr Condition, Stmt ThenBranch, Stmt? ElseBranch) : Stmt; public record Print(Expr Expr) : Stmt; // Temporary for console.log public record Break(Token Keyword, Token? Label = null) : Stmt; diff --git a/Parsing/Parser.Statements.cs b/Parsing/Parser.Statements.cs index 13d8f939..0f2c7a74 100644 --- a/Parsing/Parser.Statements.cs +++ b/Parsing/Parser.Statements.cs @@ -168,7 +168,7 @@ private Stmt ForStatement() Stmt body = Statement(); if (isOfLoop) return new Stmt.ForOf(varName, null, rhs, body, isAsync); - return new Stmt.ForIn(varName, null, rhs, body); + return new Stmt.ForIn(varName, null, rhs, body, IsDeclaration: false); } // Traditional for loop without let/const diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index f3304492..a9f1658e 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -39,7 +39,7 @@ tests/cases/conformance/es2019/importMeta/importMetaNarrowing.ts Fail tests/cases/conformance/es2020/bigintMissingES2019.ts Pass tests/cases/conformance/es2020/bigintMissingES2020.ts Pass tests/cases/conformance/es2020/bigintMissingESNext.ts Pass -tests/cases/conformance/es2020/constructBigint.ts Fail +tests/cases/conformance/es2020/constructBigint.ts Pass tests/cases/conformance/es2020/es2020IntlAPIs.ts Fail tests/cases/conformance/es2020/intlNumberFormatES2020.ts Pass tests/cases/conformance/es2020/localesObjectArgument.ts Pass @@ -71,7 +71,7 @@ tests/cases/conformance/es2023/intlNumberFormatES5UseGrouping.ts Fail tests/cases/conformance/es6/Symbols/symbolDeclarationEmit1.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit10.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit11.ts Pass -tests/cases/conformance/es6/Symbols/symbolDeclarationEmit12.ts Fail +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit12.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit13.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit14.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit2.ts Pass @@ -83,14 +83,14 @@ tests/cases/conformance/es6/Symbols/symbolDeclarationEmit7.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit8.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit9.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty1.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty10.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty11.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty12.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty10.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty11.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty12.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty13.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty14.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty15.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty16.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty17.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty17.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty18.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty19.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty2.ts Pass @@ -104,23 +104,23 @@ tests/cases/conformance/es6/Symbols/symbolProperty26.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty27.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty28.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty29.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty3.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty30.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty3.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty30.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty31.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty32.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty33.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty32.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty33.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty34.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty35.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty36.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty35.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty36.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty37.ts TypeCheckError tests/cases/conformance/es6/Symbols/symbolProperty38.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty39.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty39.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty4.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty40.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty41.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty42.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty43.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty44.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty42.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty43.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty44.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty45.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty46.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty47.ts Fail @@ -129,9 +129,9 @@ tests/cases/conformance/es6/Symbols/symbolProperty49.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty5.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty50.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty51.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty52.ts Skipped:lib-drift -tests/cases/conformance/es6/Symbols/symbolProperty53.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty54.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty52.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty53.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty54.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty55.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty56.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty57.ts Pass @@ -140,23 +140,23 @@ tests/cases/conformance/es6/Symbols/symbolProperty59.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty6.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty60.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty61.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty7.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty7.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty8.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty9.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty9.ts Pass tests/cases/conformance/es6/Symbols/symbolType1.ts Fail tests/cases/conformance/es6/Symbols/symbolType10.ts Pass tests/cases/conformance/es6/Symbols/symbolType11.ts Pass tests/cases/conformance/es6/Symbols/symbolType12.ts Pass -tests/cases/conformance/es6/Symbols/symbolType13.ts Fail -tests/cases/conformance/es6/Symbols/symbolType14.ts Fail +tests/cases/conformance/es6/Symbols/symbolType13.ts Pass +tests/cases/conformance/es6/Symbols/symbolType14.ts Pass tests/cases/conformance/es6/Symbols/symbolType15.ts Fail tests/cases/conformance/es6/Symbols/symbolType16.ts Pass tests/cases/conformance/es6/Symbols/symbolType17.ts Pass tests/cases/conformance/es6/Symbols/symbolType18.ts Pass tests/cases/conformance/es6/Symbols/symbolType19.ts Pass -tests/cases/conformance/es6/Symbols/symbolType2.ts Fail -tests/cases/conformance/es6/Symbols/symbolType20.ts Fail -tests/cases/conformance/es6/Symbols/symbolType3.ts Fail +tests/cases/conformance/es6/Symbols/symbolType2.ts Pass +tests/cases/conformance/es6/Symbols/symbolType20.ts Pass +tests/cases/conformance/es6/Symbols/symbolType3.ts Pass tests/cases/conformance/es6/Symbols/symbolType4.ts Pass tests/cases/conformance/es6/Symbols/symbolType5.ts Pass tests/cases/conformance/es6/Symbols/symbolType6.ts Pass @@ -247,17 +247,17 @@ tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/enumIsNotA tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/nullIsSubtypeOfEverythingButUndefined.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/stringLiteralTypeIsSubtypeOfString.ts Fail tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfAny.ts Pass -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameter.ts Fail +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameter.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints.ts Pass -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints2.ts Fail -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints3.ts Fail -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints4.ts Fail +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints2.ts Pass +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints3.ts Pass +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints4.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithRecursiveConstraints.ts Fail tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfUnion.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures.ts Pass -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures2.ts Fail -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures3.ts Fail -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures4.ts Fail +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures2.ts Pass +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures3.ts Pass +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures4.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignaturesA.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignaturesWithOptionalParameters.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignaturesWithRestParameters.ts Pass @@ -288,7 +288,7 @@ tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingW tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithObjectMembersOptionality2.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithObjectMembersOptionality3.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithObjectMembersOptionality4.ts Pass -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithOptionalProperties.ts Fail +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithOptionalProperties.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithStringIndexer.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithStringIndexer2.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithStringIndexer3.ts Pass diff --git a/TypeSystem/TypeChecker.Calls.cs b/TypeSystem/TypeChecker.Calls.cs index a76696b4..730ddc27 100644 --- a/TypeSystem/TypeChecker.Calls.cs +++ b/TypeSystem/TypeChecker.Calls.cs @@ -417,9 +417,11 @@ private bool TryCheckBuiltinCall(Expr.Call call, out TypeInfo result) throw new TypeCheckException("BigInt() requires exactly one argument.", tsCode: "TS2554"); } var argType = CheckExpr(call.Arguments[0]); - if (!IsNumber(argType) && !IsString(argType) && !IsBigInt(argType) && argType is not TypeInfo.Any) + // BigInt's parameter type is `string | number | bigint | boolean` (a boolean coerces to 0n/1n). + bool isBoolean = argType is TypeInfo.Primitive { Type: TokenType.TYPE_BOOLEAN } or TypeInfo.BooleanLiteral; + if (!IsNumber(argType) && !IsString(argType) && !IsBigInt(argType) && !isBoolean && argType is not TypeInfo.Any) { - throw new TypeCheckException($"BigInt() argument must be a number, string, or bigint, got '{argType}'.", tsCode: "TS2345"); + throw new TypeCheckException($"BigInt() argument must be a number, string, bigint, or boolean, got '{argType}'.", tsCode: "TS2345"); } { result = new TypeInfo.BigInt(); return true; } } diff --git a/TypeSystem/TypeChecker.Compatibility.Helpers.cs b/TypeSystem/TypeChecker.Compatibility.Helpers.cs index 68601e95..78a34e9b 100644 --- a/TypeSystem/TypeChecker.Compatibility.Helpers.cs +++ b/TypeSystem/TypeChecker.Compatibility.Helpers.cs @@ -784,7 +784,8 @@ private Dictionary CollectGenericClassMembers(TypeInfo.Generic /// generic type arguments substituted. Returns false for branded or member-less/index-less targets /// (those stay nominal). /// - private bool StructurallyAssignableToClassTarget(TypeInfo targetResolved, TypeInfo source) + private bool StructurallyAssignableToClassTarget(TypeInfo targetResolved, TypeInfo source, + bool emptyTargetAcceptsObjectSource = false) { Dictionary members; bool hasIndex; @@ -813,7 +814,11 @@ private bool StructurallyAssignableToClassTarget(TypeInfo targetResolved, TypeIn default: return false; } - if (members.Count == 0 && !hasIndex) return false; + // A member-less, index-less unbranded target is structurally `{}`. For class-vs-class this + // stays nominal (caller passes false, preserving subclass-safety), but an object-like source + // (interface value / record) IS assignable to `{}` — that's the emptyTargetAcceptsObjectSource + // path, so `interface I → empty class C` no longer spuriously fails. + if (members.Count == 0 && !hasIndex) return emptyTargetAcceptsObjectSource; return CheckStructuralCompatibility(members, source) && IndexSignaturesSatisfied(indexCarrier, source); } diff --git a/TypeSystem/TypeChecker.Compatibility.Relations.cs b/TypeSystem/TypeChecker.Compatibility.Relations.cs index f2975d08..7645b91c 100644 --- a/TypeSystem/TypeChecker.Compatibility.Relations.cs +++ b/TypeSystem/TypeChecker.Compatibility.Relations.cs @@ -93,7 +93,22 @@ private bool TryRelateTypeParameters(TypeInfo expected, TypeInfo actual, out boo return true; } var apparent = ApparentTypeOf(actualTpOnly); - result = apparent != null && IsCompatible(expected, apparent); + if (apparent == null) + { + // An unconstrained type parameter has no apparent (constraint) type. tsc still relates + // it to a target that requires NO members — an all-optional / empty object type — its + // "subtyping assumes transitivity for optional properties (99% case)" allowance. It is + // NOT assignable to a primitive, `object`, or any target with a required member. + result = expected switch + { + TypeInfo.Record rec => HasNoRequiredMembers(rec), + TypeInfo.Interface itf => !itf.HasIndexSignature && !itf.IsCallable && !itf.IsConstructable + && itf.GetAllMembers().All(m => itf.GetAllOptionalMembers().Contains(m.Key)), + _ => false + }; + return true; + } + result = IsCompatible(expected, apparent); return true; } result = false; diff --git a/TypeSystem/TypeChecker.Compatibility.cs b/TypeSystem/TypeChecker.Compatibility.cs index 3c591fdb..ddd9201b 100644 --- a/TypeSystem/TypeChecker.Compatibility.cs +++ b/TypeSystem/TypeChecker.Compatibility.cs @@ -422,6 +422,23 @@ private bool IsCompatibleCore(TypeInfo expected, TypeInfo actual) if (!_strictNullChecks && actual is TypeInfo.Null or TypeInfo.Undefined) return expected is not TypeInfo.Never; + // Namespace/module types (typeof someNamespace / typeof import(...)) have no dedicated + // relation anywhere below, so two structurally-identical namespaces — even the exact same + // one referenced twice, e.g. `true ? af : null` vs `true ? null : af` — fell through to the + // generic `return false` at the bottom, making a namespace type spuriously incompatible with + // ITSELF. Relate them structurally, like an object/record: every member `expected` exposes + // must exist on `actual` with a compatible type. + if (expected is TypeInfo.Namespace expNs && actual is TypeInfo.Namespace actNs) + { + return expNs.Types.All(kv => actNs.Types.TryGetValue(kv.Key, out var t) && IsCompatible(kv.Value, t)) + && expNs.Values.All(kv => actNs.Values.TryGetValue(kv.Key, out var v) && IsCompatible(kv.Value, v)); + } + if (expected is TypeInfo.Module expMod && actual is TypeInfo.Module actMod) + { + return expMod.Exports.All(kv => actMod.Exports.TryGetValue(kv.Key, out var v) && IsCompatible(kv.Value, v)) + && (expMod.DefaultExport is null || (actMod.DefaultExport is not null && IsCompatible(expMod.DefaultExport, actMod.DefaultExport))); + } + // Expand recursive type aliases lazily if (expected is TypeInfo.RecursiveTypeAlias expectedRTA) { @@ -970,7 +987,7 @@ actualIG.GenericDefinition is TypeInfo.GenericClass gc2 && // interface/record) are handled by the structural paths below. if (expected is TypeInfo.Instance targetInst && actual is TypeInfo.Interface or TypeInfo.Record && - StructurallyAssignableToClassTarget(targetInst.ResolvedClassType, actual)) + StructurallyAssignableToClassTarget(targetInst.ResolvedClassType, actual, emptyTargetAcceptsObjectSource: true)) { return true; } diff --git a/TypeSystem/TypeChecker.Expressions.cs b/TypeSystem/TypeChecker.Expressions.cs index 572d5e7e..30d0a8eb 100644 --- a/TypeSystem/TypeChecker.Expressions.cs +++ b/TypeSystem/TypeChecker.Expressions.cs @@ -691,6 +691,7 @@ private TypeInfo CheckObject(Expr.ObjectLiteral obj) accessorProps.Add(prop); // Getter - extract return type from annotation only (don't check body yet) + ValidateComputedPropertyNameType(prop.Key!); string name = GetAccessorMemberName(prop.Key!); getterNames.Add(name); if (prop.Value is Expr.ArrowFunction arrow && arrow.ReturnType != null) @@ -709,6 +710,7 @@ private TypeInfo CheckObject(Expr.ObjectLiteral obj) accessorProps.Add(prop); // Setter - extract parameter type from annotation only + ValidateComputedPropertyNameType(prop.Key!); string name = GetAccessorMemberName(prop.Key!); setterNames.Add(name); if (prop.Value is Expr.ArrowFunction arrow && arrow.Parameters.Count > 0 && arrow.Parameters[0].Type != null) @@ -760,10 +762,21 @@ private TypeInfo CheckObject(Expr.ObjectLiteral obj) // Infer index signature based on key type if (keyType is (TypeInfo.Symbol or TypeInfo.UniqueSymbol) && TryGetWellKnownSymbolMemberName(ck.Expression) is { } wellKnownSymbolName) + { // Symbol.iterator/toStringTag/toPrimitive/... — a named member (canonical // "@@name", matching interfaces/classes), not merged into the symbol index // signature. Merging would lose the per-symbol type (#99 Cluster B). + // TS1117: tsc rejects a second plain (non-accessor) property with the same + // canonical name — an object literal, unlike a class/interface, can't + // overload. Getter/setter pairs are exempt (handled in their own branches, + // which never touch `fields` at this point) but a second PLAIN duplicate is not. + if (!getterNames.Contains(wellKnownSymbolName) && !setterNames.Contains(wellKnownSymbolName) + && fields.ContainsKey(wellKnownSymbolName)) + RecordTypeError(new TypeCheckException( + " An object literal cannot have multiple properties with the same name.", + line: TryGetExprLine(ck.Expression), tsCode: "TS1117")); fields[wellKnownSymbolName] = valueType; + } else if (keyType is TypeInfo.String) stringIndexType = UnifyIndexTypes(stringIndexType, valueType); else if (keyType is TypeInfo.Primitive n && n.Type == TokenType.TYPE_NUMBER) @@ -780,7 +793,10 @@ private TypeInfo CheckObject(Expr.ObjectLiteral obj) // Union of string/number types - use string index signature stringIndexType = UnifyIndexTypes(stringIndexType, valueType); else - throw new TypeCheckException($" Computed property key must be string, number, or symbol, got '{keyType}'.", tsCode: "TS1170"); + // Record (don't throw) so every offending computed key in this object + // literal is reported, not just the first — mirrors the interface + // string-index-collision loop (TS2411) elsewhere in this file. + RecordTypeError(new TypeCheckException(" A computed property name must be of type 'string', 'number', 'symbol', or 'any'.", line: TryGetExprLine(ck.Expression), tsCode: "TS2464")); break; } } @@ -883,6 +899,36 @@ private static string GetAccessorMemberName(Expr.PropertyKey key) => ? wellKnownName : GetPropertyKeyNameForTypeCheck(key); + /// + /// TS2464: an object-literal accessor's computed key (get [expr]() {}/set [expr](v) {}) + /// must be of type string/number/symbol/any, mirroring tsc. The plain-property computed-key switch + /// in validates this inline (it also needs the key's type to route index + /// signatures); accessors only extract a NAME via and never + /// evaluated/validated the key's type at all, so a non-conforming accessor key silently passed. + /// Records (doesn't throw) so multiple offending members in one literal are all reported. + /// + private void ValidateComputedPropertyNameType(Expr.PropertyKey key) + { + if (key is not Expr.ComputedKey ck) return; + TypeInfo keyType = CheckExpr(ck.Expression); + if (IsValidComputedPropertyNameType(keyType)) return; + RecordTypeError(new TypeCheckException( + " A computed property name must be of type 'string', 'number', 'symbol', or 'any'.", + line: TryGetExprLine(ck.Expression), tsCode: "TS2464")); + } + + private static bool IsValidComputedPropertyNameType(TypeInfo keyType) => keyType switch + { + TypeInfo.Any => true, + TypeInfo.String => true, + TypeInfo.StringLiteral => true, + TypeInfo.Primitive { Type: TokenType.TYPE_NUMBER } => true, + TypeInfo.NumberLiteral => true, + TypeInfo.Symbol or TypeInfo.UniqueSymbol => true, + TypeInfo.Union u => u.FlattenedTypes.All(IsValidComputedPropertyNameType), + _ => false, + }; + /// /// Unifies index signature types - creates a union if types differ. /// @@ -1744,7 +1790,18 @@ private TypeInfo LookupVariable(Token name) if (name.Lexeme == "Number") return new TypeInfo.Any(); // Number is a special global object if (name.Lexeme == "String") return new TypeInfo.Any(); // String is a special global object if (name.Lexeme == "Boolean") return new TypeInfo.Any(); // Boolean is a special global object - if (name.Lexeme == "Symbol") return new TypeInfo.Any(); // Symbol is a special global object + if (name.Lexeme == "Symbol") + { + // tsc lets user code augment the global `SymbolConstructor` interface via declaration + // merging (`interface SymbolConstructor { foo: string }` adds a real `Symbol.foo`). + // Prefer a user-declared SymbolConstructor when present so those members resolve; + // otherwise fall back to the built-in shape. Not a full merge (a file combining both a + // custom member AND a built-in one like Symbol.for would only see the custom interface), + // but covers the real declaration-merging tests, which never mix the two. + if (_environment.Get("SymbolConstructor") is TypeInfo.Interface userSymbolCtor) + return userSymbolCtor; + return WellKnownSymbolTypes.SymbolConstructor; // tsc's SymbolConstructor shape + } if (name.Lexeme == "Function") return new TypeInfo.Any(); // Function is the Function constructor if (name.Lexeme == "Proxy") return new TypeInfo.Any(); // Proxy is a special global object if (name.Lexeme == "Buffer") return new TypeInfo.Any(); // Buffer is a global constructor for binary data diff --git a/TypeSystem/TypeChecker.Operators.cs b/TypeSystem/TypeChecker.Operators.cs index 7e623170..a853fca1 100644 --- a/TypeSystem/TypeChecker.Operators.cs +++ b/TypeSystem/TypeChecker.Operators.cs @@ -29,11 +29,28 @@ private TypeInfo CheckBinary(Expr.Binary binary) OperatorDescriptor.Equality => new TypeInfo.Primitive(TokenType.TYPE_BOOLEAN), OperatorDescriptor.Bitwise or OperatorDescriptor.BitwiseShift => CheckBitwiseBinary(left, right, line), OperatorDescriptor.UnsignedRightShift => CheckUnsignedShiftBinary(left, right, line), - OperatorDescriptor.In or OperatorDescriptor.InstanceOf => new TypeInfo.Primitive(TokenType.TYPE_BOOLEAN), + OperatorDescriptor.In => CheckInOperator(right, line), + // instanceof operand validation (TS2358/TS2359) is intentionally NOT done here: the + // symbolType1 case needs `Symbol() || {}` to stay a `symbol | {}` union, but CheckLogical + // currently collapses it to a bare `symbol`, which would make a bare-symbol LHS check + // fire spuriously on `(Symbol() || {}) instanceof Object`. Deferred with that inference gap. + OperatorDescriptor.InstanceOf => new TypeInfo.Primitive(TokenType.TYPE_BOOLEAN), _ => new TypeInfo.Any() }; } + /// + /// The `in` operator: the right operand must be an object / `any` / type parameter. tsc rejects + /// a symbol right operand (`"" in Symbol.toPrimitive`) with TS2322. Scoped to symbol so we don't + /// newly reject other non-object right operands the checker currently tolerates. + /// + private TypeInfo CheckInOperator(TypeInfo right, int line) + { + if (right is TypeInfo.Symbol or TypeInfo.UniqueSymbol) + throw new TypeCheckException($"Type '{right}' is not assignable to type 'object'.", line > 0 ? line : null, tsCode: "TS2322"); + return new TypeInfo.Primitive(TokenType.TYPE_BOOLEAN); + } + /// /// True if this type is Any, Inferred, or a Union containing an Any/Inferred arm. /// A union that includes 'any' is effectively permissive (JS semantics); operators @@ -285,11 +302,17 @@ private TypeInfo CheckNullishCoalescing(Expr.NullishCoalescing nc) return rightType; // null/undefined ?? right = right } - // If left (non-nullish) and right are compatible, return non-nullish left - if (IsCompatible(nonNullishLeft, rightType) || IsCompatible(rightType, nonNullishLeft)) + // Return whichever of the two is the wider type (the other is assignable to it) — same + // "pick wider of two" pattern as CheckTernary/CheckLogicalAssign; this one previously + // returned nonNullishLeft unconditionally even when rightType was the wider of the two. + if (IsCompatible(nonNullishLeft, rightType)) { return nonNullishLeft; } + if (IsCompatible(rightType, nonNullishLeft)) + { + return rightType; + } // Otherwise return union of non-nullish left and right return new TypeInfo.Union([nonNullishLeft, rightType]); @@ -393,11 +416,16 @@ private TypeInfo CheckTernary(Expr.Ternary ternary, TypeInfo? contextualType = n elseType = CheckExprWithContext(ternary.ElseBranch, contextualType); } - // Return the more specific type, or thenType if both are compatible - if (IsCompatible(thenType, elseType) || IsCompatible(elseType, thenType)) + // Return whichever branch type is the wider of the two (the other is assignable to it), + // or thenType if they're mutually compatible (e.g. identical types). + if (IsCompatible(thenType, elseType)) { return thenType; } + if (IsCompatible(elseType, thenType)) + { + return elseType; + } // Return union of both branch types return new TypeInfo.Union([thenType, elseType]); @@ -713,6 +741,15 @@ private TypeInfo CheckNonNullAssertion(Expr.NonNullAssertion nna) private TypeInfo CheckDelete(Expr.Delete delete) { + // `delete x.p` where p is a read-only property is TS2704. Detect it on the receiver's + // type before the generic operand check (e.g. `delete Symbol.iterator` — the well-known + // symbol members of SymbolConstructor are readonly). + if (delete.Operand is Expr.Get get) + { + TypeInfo recvType = CheckExpr(get.Object); + if (recvType is TypeInfo.Interface itf && itf.IsMemberReadonly(get.Name.Lexeme)) + throw new TypeCheckException("The operand of a 'delete' operator cannot be a read-only property.", line: get.Name.Line, tsCode: "TS2704"); + } // Type check the operand (for any side effects or errors) CheckExpr(delete.Operand); // delete always returns boolean diff --git a/TypeSystem/TypeChecker.Properties.Helpers.cs b/TypeSystem/TypeChecker.Properties.Helpers.cs index 9e916f65..d7f1cae2 100644 --- a/TypeSystem/TypeChecker.Properties.Helpers.cs +++ b/TypeSystem/TypeChecker.Properties.Helpers.cs @@ -233,7 +233,7 @@ private TypeInfo CheckGetOnInterface(TypeInfo.Interface itf, Token memberName) { return protoMember; } - throw new TypeCheckException($" Property '{memberName.Lexeme}' does not exist on interface '{itf.Name}'.", tsCode: "TS2339"); + throw new TypeCheckException($" Property '{memberName.Lexeme}' does not exist on interface '{itf.Name}'.", line: memberName.Line, tsCode: "TS2339"); } /// diff --git a/TypeSystem/TypeChecker.Properties.Index.cs b/TypeSystem/TypeChecker.Properties.Index.cs index 41e2cf15..0edf5023 100644 --- a/TypeSystem/TypeChecker.Properties.Index.cs +++ b/TypeSystem/TypeChecker.Properties.Index.cs @@ -33,6 +33,15 @@ private TypeInfo CheckGetIndex(Expr.GetIndex getIndex) return new TypeInfo.Any(); } + // TS2538: a value whose type can't be a property key — e.g. a function type such as + // `Symbol.for` (the method, not a call) — cannot be used as an index. This is distinct from + // the implicit-any TS7053 emitted below for a valid-but-unmodeled key. + if (indexType is TypeInfo.Function or TypeInfo.OverloadedFunction) + { + throw new TypeCheckException($" Type '{indexType}' cannot be used as an index type.", + line: TryGetExprLine(getIndex.Index), tsCode: "TS2538"); + } + // Indexing a method's inferred-return-type placeholder (still being resolved during // body checking, e.g. calling a sibling static method from a ternary). Treat as any. if (objType is TypeInfo.Inferred) diff --git a/TypeSystem/TypeChecker.Properties.New.cs b/TypeSystem/TypeChecker.Properties.New.cs index d5cf0c7a..20545214 100644 --- a/TypeSystem/TypeChecker.Properties.New.cs +++ b/TypeSystem/TypeChecker.Properties.New.cs @@ -88,6 +88,13 @@ private TypeInfo CheckNew(Expr.New newExpr) bool isSimpleName = IsSimpleIdentifier(newExpr.Callee); string? simpleClassName = GetSimpleClassName(newExpr.Callee); + // `new Symbol()` — the Symbol function's call signature returns a (non-void) symbol and it + // has no construct signature, so calling it with `new` is TS2350. + if (isSimpleName && simpleClassName == "Symbol") + { + throw new TypeCheckException("Only a void function can be called with the 'new' keyword.", tsCode: "TS2350"); + } + // Handle new Date() constructor if (isSimpleName && simpleClassName == "Date") { diff --git a/TypeSystem/TypeChecker.Properties.cs b/TypeSystem/TypeChecker.Properties.cs index 45d0a6b7..8f14199c 100644 --- a/TypeSystem/TypeChecker.Properties.cs +++ b/TypeSystem/TypeChecker.Properties.cs @@ -107,6 +107,21 @@ private TypeInfo CheckGet(Expr.Get get) var wellKnownType = WellKnownSymbolTypes.TryGet(get.Name.Lexeme); if (wellKnownType != null) return wellKnownType; + + // Symbol.for/keyFor/prototype referenced as plain values (not called) — mirrors the + // typed signatures TryCheckBuiltinCall already gives the CALL form of Symbol.for/keyFor. + // Without this, bare `Symbol.for` fell through to CheckExpr(Symbol)=Any then `.for` on + // Any=Any, silently accepting it wherever a real, non-`any` type is required (e.g. a + // computed property name, TS2464) even though tsc types it as a real function/object. + switch (get.Name.Lexeme) + { + case "for": + return new TypeInfo.Function([new TypeInfo.String()], new TypeInfo.Symbol(), 1, false, null, ["key"]); + case "keyFor": + return new TypeInfo.Function([new TypeInfo.Symbol()], new TypeInfo.Union([new TypeInfo.String(), new TypeInfo.Undefined()]), 1, false, null, ["sym"]); + case "prototype": + return new TypeInfo.Record(FrozenDictionary.Empty); + } } // Check for property narrowing (e.g., after "if (obj.prop !== null)" or nested "obj.a.b") @@ -700,7 +715,7 @@ private TypeInfo CheckSet(Expr.Set set) return valueType; } } - throw new TypeCheckException($" Property '{set.Name.Lexeme}' does not exist on interface '{itf.Name}'.", tsCode: "TS2339"); + throw new TypeCheckException($" Property '{set.Name.Lexeme}' does not exist on interface '{itf.Name}'.", line: set.Name.Line, tsCode: "TS2339"); } // Handle Error property assignment (name, message, stack are mutable strings; cause is any) if (objType is TypeInfo.Error) @@ -818,7 +833,7 @@ private TypeInfo CheckGetOnType(TypeInfo objType, Token memberName) return member.Value; } } - throw new TypeCheckException($" Property '{memberName.Lexeme}' does not exist on interface '{itf.Name}'.", tsCode: "TS2339"); + throw new TypeCheckException($" Property '{memberName.Lexeme}' does not exist on interface '{itf.Name}'.", line: memberName.Line, tsCode: "TS2339"); } // Handle Record type - check fields and index signatures diff --git a/TypeSystem/TypeChecker.Statements.Classes.cs b/TypeSystem/TypeChecker.Statements.Classes.cs index a8b58cd9..b13590aa 100644 --- a/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/TypeSystem/TypeChecker.Statements.Classes.cs @@ -25,6 +25,18 @@ public partial class TypeChecker _ => null }; + /// + /// The member-dictionary key for a class field: a well-known-symbol computed key + /// ([Symbol.iterator]) canonicalizes to its @@name so it matches the same member on + /// an interface/base class in structural comparison; everything else keeps its lexeme. Without this + /// a symbol-keyed field is stored under the synthetic <computed> lexeme and never lines + /// up with the interface's @@iterator, producing spurious missing-property errors. + /// + private static string GetFieldMemberName(Stmt.Field field) => + field.ComputedKey != null + ? (TryGetWellKnownSymbolMemberName(field.ComputedKey) ?? field.Name.Lexeme) + : field.Name.Lexeme; + private void CheckClassDeclaration(Stmt.Class classStmt) { // Check class decorators @@ -60,6 +72,18 @@ private void CheckClassDeclaration(Stmt.Class classStmt) using (new EnvironmentScope(this, classTypeEnv)) superclass = ResolveDeclaredSuperclass(classStmt); + // `class C1 extends C2 {} class C2 {}` — a class value referenced in its own or an earlier + // class's extends clause, before that class is textually declared, is TS2449 (class values + // aren't hoisted for this position). Recorded (not thrown) so the body is still checked. + if (classStmt.SuperclassExpr is Expr.Variable baseVar + && _classDeclarationLines.TryGetValue(baseVar.Name.Lexeme, out int baseLine) + && baseLine > classStmt.Name.Line) + { + RecordTypeError(new TypeCheckException( + $"Class '{baseVar.Name.Lexeme}' used before its declaration.", + line: classStmt.Name.Line, tsCode: "TS2449")); + } + // Create mutable class early so self-references in method return types work. // This allows methods like "next(): Node" to correctly resolve the return type. // The mutable class is populated during signature collection and frozen at the end. @@ -120,27 +144,78 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) // Computed symbol-keyed methods (`[Symbol.iterator]() {...}`) are modeled under their canonical // well-known-symbol member name (@@iterator, @@asyncIterator, …) so structural iterability and // member lookup see them (#592/#485). They carry the synthetic `` name, so they are - // pulled out of the name-keyed overload grouping below. Arbitrary computed keys (e.g. `[v]()`) - // aren't statically named members and are skipped (still parsed/checked, just untyped — like - // computed accessors). - foreach (var method in classStmt.Methods.Where(m => m.ComputedKey != null && m.Body != null)) + // pulled out of the name-keyed overload grouping below and grouped here instead by (IsStatic, + // canonical name) — running the SAME overload/duplicate-implementation/static-consistency + // validation string-named methods get, rather than silently "last one wins" registration. + // Arbitrary computed keys (e.g. `[v]()`) aren't statically named members and are skipped + // (still parsed/checked, just untyped — like computed accessors). + var computedMethodGroups = classStmt.Methods + .Where(m => m.ComputedKey != null) + .Select(m => (Method: m, Name: TryGetWellKnownSymbolMemberName(m.ComputedKey))) + .Where(x => x.Name != null) + // Grouped by canonical NAME ONLY (not IsStatic too): the static-consistency check + // below needs static AND instance declarations of the same name in one group to + // compare them against each other — grouping by (IsStatic, Name) would silently + // split a static/instance mismatch into two single-member groups that never meet. + .GroupBy(x => x.Name!) + .ToList(); + + foreach (var group in computedMethodGroups) { - if (TryGetWellKnownSymbolMemberName(method.ComputedKey) is not { } memberName) - continue; - // Build the factory signature WITHOUT the generator-return wrapping BuildMethodFuncType - // applies: a [Symbol.iterator]()/[Symbol.asyncIterator]() factory must return the iterator - // type itself (e.g. `Iterator` — what for...of consumes and reads its element from), - // not Generator>. An un-annotated generator factory stays here + string memberName = group.Key; + var members = group.Select(x => x.Method).ToList(); + + // TS2387/TS2388: tsc requires every declaration in an overload group to agree on + // static-ness. It compares consecutive pairs in source order and reports the mismatch + // at the LATER member, choosing the message from the EARLIER member's own static-ness + // ("must be static" when the earlier one was static, "must not be static" otherwise) — + // verified against symbolProperty42's exact (line, code) pairs. + for (int i = 1; i < members.Count; i++) + { + if (members[i - 1].IsStatic != members[i].IsStatic) + { + var (msg, code) = members[i - 1].IsStatic + ? (" Function overload must be static.", "TS2387") + : (" Function overload must not be static.", "TS2388"); + RecordTypeError(new TypeCheckException(msg, line: members[i].Name.Line, tsCode: code)); + } + } + + var signatures = members.Where(m => m.Body == null && !m.IsAbstract).ToList(); + var implementations = members.Where(m => m.Body != null).ToList(); + + if (implementations.Count > 1) + { + // tsc flags EVERY declaration in the group once a second implementation appears — + // signatures included, not just the implementations. + foreach (var m in members) + RecordTypeError(new TypeCheckException(" Duplicate function implementation.", line: m.Name.Line, tsCode: "TS2393")); + } + else if (implementations.Count == 0 && signatures.Count > 0) + { + RecordTypeError(new TypeCheckException( + " Function implementation is missing or not immediately following the declaration.", + line: members[^1].Name.Line, tsCode: "TS2391")); + } + + // Register the group's type from the single implementation when there is exactly one + // (the common, error-free case), else the last declaration as a best-effort fallback so + // an error'd group still gets SOME type. Build the factory signature WITHOUT the + // generator-return wrapping BuildMethodFuncType applies: a [Symbol.iterator]()/ + // [Symbol.asyncIterator]() factory must return the iterator type itself (e.g. + // `Iterator` — what for...of consumes and reads its element from), not + // Generator>. An un-annotated generator factory stays here // and is filled in by the inferred-return post-pass below as Generator. + var representative = implementations.Count == 1 ? implementations[0] : members[^1]; var (cParamTypes, cRequired, cHasRest, cParamNames) = BuildFunctionSignature( - method.Parameters, validateDefaults: true, contextName: $"method '{memberName}'"); - TypeInfo factoryReturn = ResolveAnnotation(method.ReturnType, method.ReturnTypeNode) ?? new TypeInfo.Inferred(); + representative.Parameters, validateDefaults: true, contextName: $"method '{memberName}'"); + TypeInfo factoryReturn = ResolveAnnotation(representative.ReturnType, representative.ReturnTypeNode) ?? new TypeInfo.Inferred(); var funcType = new TypeInfo.Function(cParamTypes, factoryReturn, cRequired, cHasRest, null, cParamNames); - if (method.IsStatic) + if (representative.IsStatic) mutableClass.StaticMethods[memberName] = funcType; else mutableClass.Methods[memberName] = funcType; - (method.IsStatic ? mutableClass.StaticMethodAccess : mutableClass.MethodAccess)[memberName] = method.Access; + (representative.IsStatic ? mutableClass.StaticMethodAccess : mutableClass.MethodAccess)[memberName] = representative.Access; } // First pass: collect signatures, grouping overloads @@ -248,10 +323,20 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) DecoratorTarget fieldTarget = field.IsStatic ? DecoratorTarget.StaticField : DecoratorTarget.Field; CheckDecorators(field.Decorators, fieldTarget); - string fieldName = field.Name.Lexeme; + string fieldName = GetFieldMemberName(field); TypeInfo fieldType = ResolveAnnotation(field.TypeAnnotation, field.TypeAnnotationNode) ?? new TypeInfo.Any(); + // TS1166: a computed DATA-property name (a class field, unlike a method/accessor) must be a + // literal type or a 'unique symbol'. A plain, non-unique `symbol` — e.g. `[Symbol()]` — + // is not allowed; a well-known symbol (`[Symbol.iterator]`) is a unique symbol, so it's fine. + if (field.ComputedKey != null && CheckExpr(field.ComputedKey) is TypeInfo.Symbol) + { + RecordTypeError(new TypeCheckException( + "A computed property name in a class property declaration must have a simple literal type or a 'unique symbol' type.", + line: TryGetExprLine(field.ComputedKey) ?? field.Name.Line, tsCode: "TS1166")); + } + // Handle ES2022 private fields (#field) if (field.IsPrivate) { @@ -285,6 +370,13 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) // Collect accessor types if (classStmt.Accessors != null) { + // TS2300: two getters (or two setters) with the same name/static-ness are a duplicate + // identifier — a class, unlike an object literal, can't have two same-kind accessors for + // one member at all (a get+set PAIR is fine; that's tracked separately below). Covers both + // plain and well-known-symbol computed names (`get [Symbol.hasInstance]()` declared twice), + // neither of which had any duplicate-accessor detection before. + var seenGetters = new Dictionary<(bool IsStatic, string Name), int>(); + var seenSetters = new Dictionary<(bool IsStatic, string Name), int>(); foreach (var accessor in classStmt.Accessors) { // Check accessor decorators @@ -293,6 +385,26 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) : DecoratorTarget.Setter; CheckDecorators(accessor.Decorators, accessorTarget); + string? canonicalName = accessor.ComputedKey != null + ? TryGetWellKnownSymbolMemberName(accessor.ComputedKey) + : accessor.Name.Lexeme; + if (canonicalName != null) + { + int line = TryGetExprLine(accessor.ComputedKey) ?? accessor.Name.Line; + string displayName = accessor.ComputedKey != null ? $"[Symbol.{canonicalName["@@".Length..]}]" : canonicalName; + // A method already registered under this name (methods are processed above) + // makes ANY accessor of the same name a duplicate too — a class member can't be + // both a method and an accessor. tsc flags the accessor, not the method. + var methodDict = accessor.IsStatic ? mutableClass.StaticMethods : mutableClass.Methods; + var seen = accessor.Kind.Type == TokenType.GET ? seenGetters : seenSetters; + var key = (accessor.IsStatic, canonicalName); + if (methodDict.ContainsKey(canonicalName) || !seen.TryAdd(key, line)) + { + RecordTypeError(new TypeCheckException( + $" Duplicate identifier '{displayName}'.", line: line, tsCode: "TS2300")); + } + } + // Computed accessor names (get [Symbol.x]()) have no static member // name to register; their bodies are still checked later. if (accessor.ComputedKey != null) @@ -578,8 +690,8 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) TypeInfo initType = CheckExpr(field.Initializer); // For ES2022 private static fields, look in StaticPrivateFieldTypes TypeInfo staticFieldDeclaredType = field.IsPrivate - ? classTypeForBody.StaticPrivateFieldTypes[field.Name.Lexeme] - : classTypeForBody.StaticProperties[field.Name.Lexeme]; + ? classTypeForBody.StaticPrivateFieldTypes[GetFieldMemberName(field)] + : classTypeForBody.StaticProperties[GetFieldMemberName(field)]; if (!IsCompatible(staticFieldDeclaredType, initType)) { throw new TypeCheckException($" Cannot assign type '{initType}' to static property '{field.Name.Lexeme}' of type '{staticFieldDeclaredType}'.", tsCode: "TS2322"); @@ -676,7 +788,7 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) if (field.IsStatic || field.Initializer == null) continue; TypeInfo initType = CheckExpr(field.Initializer); var declaredTypes = field.IsPrivate ? classTypeForBody.PrivateFieldTypes : classTypeForBody.FieldTypes; - if (declaredTypes.TryGetValue(field.Name.Lexeme, out var fieldDeclaredType) + if (declaredTypes.TryGetValue(GetFieldMemberName(field), out var fieldDeclaredType) && fieldDeclaredType is not (TypeInfo.Inferred or TypeInfo.Any) && !IsCompatible(fieldDeclaredType, initType)) { @@ -989,6 +1101,12 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) _compatibilityCache = null; _identityCompatibilityCache = null; } + + // Symbol-index conformance (TS2411) runs HERE — after method bodies — so an un-annotated + // `[Symbol.x]()` method carries its inferred return type rather than the placeholder + // it held at the string-index check earlier (ValidateClassPropertiesAgainstIndex). + mutableClass.ResetFrozenCache(); + ValidateClassMembersAgainstSymbolIndex(classStmt, mutableClass.Freeze()); } /// @@ -1237,22 +1355,23 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) // Collect field types foreach (var field in classStmt.Fields) { + string fieldName = GetFieldMemberName(field); TypeInfo fieldType = ResolveAnnotation(field.TypeAnnotation, field.TypeAnnotationNode) ?? new TypeInfo.Any(); if (field.IsStatic) { - mutableClass.StaticProperties[field.Name.Lexeme] = fieldType; + mutableClass.StaticProperties[fieldName] = fieldType; } else { - mutableClass.FieldTypes[field.Name.Lexeme] = fieldType; + mutableClass.FieldTypes[fieldName] = fieldType; } - (field.IsStatic ? mutableClass.StaticFieldAccess : mutableClass.FieldAccess)[field.Name.Lexeme] = field.Access; + (field.IsStatic ? mutableClass.StaticFieldAccess : mutableClass.FieldAccess)[fieldName] = field.Access; if (field.IsReadonly) { - mutableClass.ReadonlyFields.Add(field.Name.Lexeme); + mutableClass.ReadonlyFields.Add(fieldName); } } diff --git a/TypeSystem/TypeChecker.Statements.Functions.cs b/TypeSystem/TypeChecker.Statements.Functions.cs index 67c26de3..9b8b5ec5 100644 --- a/TypeSystem/TypeChecker.Statements.Functions.cs +++ b/TypeSystem/TypeChecker.Statements.Functions.cs @@ -227,6 +227,13 @@ private void HoistLexicalDeclarations(IEnumerable statements) /// reference classes defined later in the same file. The real class type is defined when /// CheckClassDeclaration runs during the sequential pass, overwriting the Any placeholder. /// + /// + /// Source line of each class's textual declaration, keyed by name. Populated during hoisting so + /// a later extends clause can detect a forward reference (TS2449 — using a class before + /// its declaration; class values, unlike types, are not hoisted for that position). + /// + private readonly Dictionary _classDeclarationLines = []; + private void HoistClassDeclarations(IEnumerable statements) { foreach (var stmt in statements) @@ -234,10 +241,12 @@ private void HoistClassDeclarations(IEnumerable statements) switch (stmt) { case Stmt.Class cls: + _classDeclarationLines[cls.Name.Lexeme] = cls.Name.Line; if (!_environment.IsDefinedLocally(cls.Name.Lexeme)) _environment.Define(cls.Name.Lexeme, new TypeInfo.Any()); break; case Stmt.Export { Declaration: Stmt.Class exportCls }: + _classDeclarationLines[exportCls.Name.Lexeme] = exportCls.Name.Line; if (!_environment.IsDefinedLocally(exportCls.Name.Lexeme)) _environment.Define(exportCls.Name.Lexeme, new TypeInfo.Any()); break; @@ -339,9 +348,16 @@ private void HoistConstFunctionExpression(Token name, string? typeAnnotation, Ex } else { - // No return type specified - use Any for hoisting - // The actual type will be checked during normal processing - returnType = new TypeInfo.Any(); + // Neither the binding nor the arrow annotates a return type. A precise + // `(params) => any` placeholder here looks like a REAL prior declaration to + // VisitVar's TS2403 redeclaration check once the real (usually non-`any`) + // inferred return type is established for this var's OWN first-and-only + // declaration — falsely tripping "subsequent variable declarations must have + // the same type". Register a bare `any` instead, matching how + // HoistVarDeclarations/HoistClassDeclarations placeholder forward references — + // `any` is already exempt from that check. + _environment.Define(name.Lexeme, new TypeInfo.Any()); + return; } // Handle 'this' type diff --git a/TypeSystem/TypeChecker.Statements.Interfaces.cs b/TypeSystem/TypeChecker.Statements.Interfaces.cs index 3799913a..ad555789 100644 --- a/TypeSystem/TypeChecker.Statements.Interfaces.cs +++ b/TypeSystem/TypeChecker.Statements.Interfaces.cs @@ -186,6 +186,13 @@ private void PreRegisterInterface(Stmt.Interface interfaceStmt) private void CheckInterfaceDeclaration(Stmt.Interface interfaceStmt) { + // An interface may not be named after a primitive type keyword (TS2427). `symbol` is a + // contextual keyword the parser accepts as an identifier, so it reaches here. + if (interfaceStmt.Name.Lexeme == "symbol") + { + throw new TypeCheckException("Interface name cannot be 'symbol'.", line: interfaceStmt.Name.Line, tsCode: "TS2427"); + } + // Handle generic type parameters with two-pass approach to support recursive constraints (e.g., T extends TreeNode) List? interfaceTypeParams = null; TypeEnvironment interfaceTypeEnv = new(_environment); @@ -343,6 +350,26 @@ private void CheckInterfaceDeclaration(Stmt.Interface interfaceStmt) } } } + + // Mirror image of the string-index check above: a symbol index signature only + // constrains computed well-known-symbol members (canonical "@@name"), never + // plain string/number-keyed ones. + if (symbolIndexType != null) + { + foreach (var (name, type) in members) + { + if (!name.StartsWith("@@", StringComparison.Ordinal)) continue; + if (!IsCompatible(symbolIndexType, type)) + { + memberLines.TryGetValue(name, out var line); + string displayName = $"[Symbol.{name["@@".Length..]}]"; + RecordTypeError(new TypeCheckException( + $" Property '{displayName}' of type '{type}' is not assignable to 'symbol' index type '{symbolIndexType}'.", + line: line == 0 ? interfaceStmt.Name.Line : line, + tsCode: "TS2411")); + } + } + } } } @@ -406,6 +433,32 @@ private void CheckInterfaceDeclaration(Stmt.Interface interfaceStmt) } finally { _extendsClauseConstraintLine = savedExtendsLine; } extends = extendsList.ToFrozenSet(); + + // TS2320: an interface may not simultaneously extend two bases that declare the same + // member with non-identical types. Two types are treated as "not identical" when they + // aren't mutually assignable — conservative, so a genuinely-shared (diamond) member, + // being mutually assignable, never trips it. + var seenBaseMembers = new Dictionary(); + bool reportedExtendConflict = false; + foreach (var baseItf in extendsList) + { + if (reportedExtendConflict) break; + foreach (var (mName, mType) in baseItf.GetAllMembers()) + { + if (seenBaseMembers.TryGetValue(mName, out var prev) + && prev.BaseName != baseItf.Name + && !(IsCompatible(prev.Type, mType) && IsCompatible(mType, prev.Type))) + { + string display = mName.StartsWith("@@") ? $"[Symbol.{mName[2..]}]" : mName; + RecordTypeError(new TypeCheckException( + $" Interface '{interfaceStmt.Name.Lexeme}' cannot simultaneously extend types '{prev.BaseName}' and '{baseItf.Name}'. Named property '{display}' of types '{prev.BaseName}' and '{baseItf.Name}' are not identical.", + line: interfaceStmt.Name.Line, tsCode: "TS2320")); + reportedExtendConflict = true; + break; + } + seenBaseMembers.TryAdd(mName, (baseItf.Name, mType)); + } + } } // Process call signatures diff --git a/TypeSystem/TypeChecker.Statements.cs b/TypeSystem/TypeChecker.Statements.cs index 959e714c..584a9e9f 100644 --- a/TypeSystem/TypeChecker.Statements.cs +++ b/TypeSystem/TypeChecker.Statements.cs @@ -966,11 +966,29 @@ internal VoidResult VisitForIn(Stmt.ForIn stmt) { TypeInfo objType = CheckExpr(stmt.Object); + // The right-hand side must be an object type / `any` / a type parameter; a symbol fails (TS2407). + if (ContainsSymbolType(objType)) + { + throw new TypeCheckException( + $"The right-hand side of a 'for...in' statement must be of type 'any', an object type or a type parameter, but here has type '{objType}'.", + line: stmt.Variable.Line, tsCode: "TS2407"); + } + if (objType is not (TypeInfo.Record or TypeInfo.Instance or TypeInfo.Array or TypeInfo.Any or TypeInfo.Class)) { throw new TypeCheckException($"'for...in' requires an object, got {objType}", tsCode: "TS2549"); } + // When the loop variable is an existing lvalue (not a fresh `var`/`let` binding), the loop + // writes string keys into it — a symbol-typed target fails (TS2405, "must be string or any"). + if (!stmt.IsDeclaration && _environment.Get(stmt.Variable.Lexeme) is TypeInfo existingTarget + && existingTarget is TypeInfo.Symbol or TypeInfo.UniqueSymbol) + { + throw new TypeCheckException( + "The left-hand side of a 'for...in' statement must be of type 'string' or 'any'.", + line: stmt.Variable.Line, tsCode: "TS2405"); + } + TypeEnvironment forInEnv = new(_environment); forInEnv.Define(stmt.Variable.Lexeme, new TypeInfo.String()); diff --git a/TypeSystem/TypeChecker.Validation.cs b/TypeSystem/TypeChecker.Validation.cs index 33d461f8..26288096 100644 --- a/TypeSystem/TypeChecker.Validation.cs +++ b/TypeSystem/TypeChecker.Validation.cs @@ -553,6 +553,99 @@ private void ValidateClassPropertiesAgainstIndex(Stmt.Class classStmt, TypeInfo. } } } + /// + /// Validates that a class's symbol-keyed members (own and inherited) are assignable to its + /// effective [s: symbol] index type (own or inherited) — TS2411, the symbol-index analogue + /// of . Runs after method bodies so inferred + /// return types are resolved. tsc attributes the diagnostic to whichever of {member, index + /// signature} is declared ON this class: an OWN member reports at the member, an INHERITED member + /// (necessarily paired with an OWN index signature) reports at the index signature. A member+index + /// pair both inherited from the same base was already reported on that base and is skipped. + /// + private void ValidateClassMembersAgainstSymbolIndex(Stmt.Class classStmt, TypeInfo.Class classType) + { + TypeInfo? ownSymIndex = classType.SymbolIndexType; + TypeInfo? effectiveIndex = ownSymIndex ?? FindSymbolIndexInBaseChain(classType.Superclass); + if (effectiveIndex is null) return; + + void CheckOwnMember(string atName, TypeInfo memberType, int line) + { + if (!IsCompatible(effectiveIndex, memberType)) + RecordTypeError(new TypeCheckException( + $" Property '[Symbol.{atName[2..]}]' of type '{memberType}' is not assignable to 'symbol' index type '{effectiveIndex}'.", + line: line, tsCode: "TS2411")); + } + + // Own symbol-keyed members: methods and fields whose computed key is a well-known symbol. + var ownNames = new HashSet(); + foreach (var method in classStmt.Methods) + { + if (method.ComputedKey is null) continue; + string? name = TryGetWellKnownSymbolMemberName(method.ComputedKey); + if (name is null || !classType.Methods.TryGetValue(name, out var mType)) continue; + ownNames.Add(name); + CheckOwnMember(name, mType, TryGetExprLine(method.ComputedKey) ?? method.Name.Line); + } + foreach (var field in classStmt.Fields) + { + if (field.IsStatic || field.ComputedKey is null) continue; + string? name = TryGetWellKnownSymbolMemberName(field.ComputedKey); + if (name is null || !classType.FieldTypes.TryGetValue(name, out var fType)) continue; + ownNames.Add(name); + CheckOwnMember(name, fType, TryGetExprLine(field.ComputedKey) ?? field.Name.Line); + } + + // Inherited symbol-keyed members are checked only when the index signature is OWN to this + // class (otherwise the member+index pair is fully inherited and the base already reported). + // They are attributed to this class's index-signature line. + if (ownSymIndex is not null) + { + int? indexLine = classStmt.IndexSignatures? + .FirstOrDefault(s => s.KeyType == TokenType.TYPE_SYMBOL)?.KeyName.Line; + foreach (var (name, mType) in CollectInheritedSymbolMembers(classType.Superclass)) + { + if (ownNames.Contains(name)) continue; // overridden here → own-member path handled it + if (!IsCompatible(ownSymIndex, mType)) + RecordTypeError(new TypeCheckException( + $" Property '[Symbol.{name[2..]}]' of type '{mType}' is not assignable to 'symbol' index type '{ownSymIndex}'.", + line: indexLine, tsCode: "TS2411")); + } + } + } + + private static TypeInfo? GetSymbolIndexType(TypeInfo? classType) => classType switch + { + TypeInfo.Class c => c.SymbolIndexType, + TypeInfo.InstantiatedGeneric { GenericDefinition: TypeInfo.GenericClass gc } => gc.SymbolIndexType, + TypeInfo.GenericClass g => g.SymbolIndexType, + _ => null + }; + + private TypeInfo? FindSymbolIndexInBaseChain(TypeInfo? baseType) + { + for (TypeInfo? current = baseType; current != null; current = GetSuperclass(current)) + if (GetSymbolIndexType(current) is { } idx) return idx; + return null; + } + + /// Symbol-keyed (@@name) methods and fields inherited from the base-class chain, + /// each yielded once (nearest override wins). + private IEnumerable<(string Name, TypeInfo Type)> CollectInheritedSymbolMembers(TypeInfo? baseType) + { + var seen = new HashSet(); + for (TypeInfo? current = baseType; current != null; current = GetSuperclass(current)) + { + if (GetMethods(current) is { } methods) + foreach (var kv in methods) + if (kv.Key.StartsWith("@@") && seen.Add(kv.Key)) + yield return (kv.Key, kv.Value); + if (GetFieldTypes(current) is { } fields) + foreach (var kv in fields) + if (kv.Key.StartsWith("@@") && seen.Add(kv.Key)) + yield return (kv.Key, kv.Value); + } + } + /// /// Walks the base-class chain looking for the accessibility of a field, getter or method named /// . Returns its , or null if absent. diff --git a/TypeSystem/WellKnownSymbolTypes.cs b/TypeSystem/WellKnownSymbolTypes.cs index f4ec38f4..ef7333fd 100644 --- a/TypeSystem/WellKnownSymbolTypes.cs +++ b/TypeSystem/WellKnownSymbolTypes.cs @@ -1,3 +1,5 @@ +using System.Collections.Frozen; + namespace SharpTS.TypeSystem; /// @@ -61,4 +63,47 @@ public static class WellKnownSymbolTypes "split" => Split, _ => null }; + + /// + /// The type of the bare `Symbol` value itself (tsc's `SymbolConstructor`). Used wherever + /// `Symbol` is referenced WITHOUT being called — e.g. `var s = Symbol; obj[s]`. Deliberately + /// NOT `any`: a bare-Symbol-derived value must fail non-permissive checks like a computed + /// property name's "must be string/number/symbol/any" validation (TS2464), the way tsc's real + /// SymbolConstructor object type does. `Symbol(...)` calls and `Symbol.iterator`-style + /// well-known-symbol access are matched by earlier special cases (TryCheckBuiltinCall / + /// CheckGet) before generic member lookup ever consults this type, so adding it here doesn't + /// change those paths — only genuine "use Symbol as a plain value" sites. + /// + public static readonly TypeInfo.Interface SymbolConstructor = new( + "SymbolConstructor", + new Dictionary + { + ["iterator"] = Iterator, + ["asyncIterator"] = AsyncIterator, + ["toStringTag"] = ToStringTag, + ["hasInstance"] = HasInstance, + ["isConcatSpreadable"] = IsConcatSpreadable, + ["toPrimitive"] = ToPrimitive, + ["species"] = Species, + ["unscopables"] = Unscopables, + ["dispose"] = Dispose, + ["asyncDispose"] = AsyncDispose, + ["match"] = Match, + ["matchAll"] = MatchAll, + ["replace"] = Replace, + ["search"] = Search, + ["split"] = Split, + ["for"] = new TypeInfo.Function([new TypeInfo.String()], new TypeInfo.Symbol(), 1, false, null, ["key"]), + ["keyFor"] = new TypeInfo.Function([new TypeInfo.Symbol()], new TypeInfo.Union([new TypeInfo.String(), new TypeInfo.Undefined()]), 1, false, null, ["sym"]), + ["prototype"] = new TypeInfo.Record(FrozenDictionary.Empty), + }.ToFrozenDictionary(), + FrozenSet.Empty, + // The well-known symbol members are `readonly` in lib.d.ts — `delete Symbol.iterator` + // is TS2704, and reassigning them is an error too. + ReadonlyMembers: new[] + { + "iterator", "asyncIterator", "toStringTag", "hasInstance", "isConcatSpreadable", + "toPrimitive", "species", "unscopables", "dispose", "asyncDispose", + "match", "matchAll", "replace", "search", "split", + }.ToFrozenSet()); }