Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions SharpTS.TypeScriptConformance/baselines/interpreted.txt
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@ tests/cases/conformance/es6/Symbols/symbolProperty7.ts Fail
tests/cases/conformance/es6/Symbols/symbolProperty8.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty9.ts Fail
tests/cases/conformance/es6/Symbols/symbolType1.ts Fail
tests/cases/conformance/es6/Symbols/symbolType10.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 Fail
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/symbolType15.ts Fail
Expand All @@ -157,11 +157,11 @@ 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/symbolType4.ts Fail
tests/cases/conformance/es6/Symbols/symbolType5.ts Fail
tests/cases/conformance/es6/Symbols/symbolType6.ts Fail
tests/cases/conformance/es6/Symbols/symbolType7.ts Fail
tests/cases/conformance/es6/Symbols/symbolType8.ts Fail
tests/cases/conformance/es6/Symbols/symbolType4.ts Pass
tests/cases/conformance/es6/Symbols/symbolType5.ts Pass
tests/cases/conformance/es6/Symbols/symbolType6.ts Pass
tests/cases/conformance/es6/Symbols/symbolType7.ts Pass
tests/cases/conformance/es6/Symbols/symbolType8.ts Pass
tests/cases/conformance/es6/Symbols/symbolType9.ts Fail
tests/cases/conformance/esnext/logicalAssignment/logicalAssignment11.ts Pass
tests/cases/conformance/types/conditional/conditionalTypes1.ts Pass
Expand Down
68 changes: 56 additions & 12 deletions TypeSystem/TypeChecker.Calls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ private TypeInfo CheckCall(Expr.Call call)
calleeType = nonNullish.Count == 1 ? nonNullish[0] : new TypeInfo.Union(nonNullish);
}

// Non-optional call on a genuinely nullable callee: tsc rejects this outright (TS2721/2722/
// 2723) rather than silently allowing it — unlike the union-of-function-members branch further
// below (which permits reading a possibly-missing PROPERTY that happens to be a function),
// actually INVOKING a possibly-null/undefined value is always an error.
if (!call.Optional && calleeType is TypeInfo.Union nullableCallee
&& (nullableCallee.ContainsNull || nullableCallee.ContainsUndefined))
{
throw CannotInvokeNullishError(nullableCallee, call.Paren.Line);
}

if (calleeType is TypeInfo.Class classType)
{
return new TypeInfo.Instance(classType);
Expand Down Expand Up @@ -299,18 +309,8 @@ private TypeInfo CheckCall(Expr.Call call)
}
}

// Also include undefined if the union has non-function members
// (e.g., the property might be missing on some types)
var nonFunctionMembers = unionCallee.FlattenedTypes
.Where(t => t is not TypeInfo.Function and not TypeInfo.OverloadedFunction and not TypeInfo.GenericFunction)
.ToList();
if (nonFunctionMembers.Any(t => t is TypeInfo.Undefined))
{
// Calling undefined would fail at runtime, but for type checking,
// we allow it to match TypeScript's behavior and add undefined to result
returnTypes.Add(new TypeInfo.Undefined());
}

// No undefined/null member can survive to here: a non-optional call on a nullable
// union already threw above, and an optional call already stripped them.
var unique = returnTypes.Distinct(TypeInfoEqualityComparer.Instance).ToList();
return unique.Count == 1 ? unique[0] : new TypeInfo.Union(unique);
}
Expand All @@ -319,6 +319,22 @@ private TypeInfo CheckCall(Expr.Call call)
throw new TypeCheckException($"Can only call functions.", tsCode: "TS2349");
}

/// <summary>
/// Builds the "cannot invoke" error for a non-optional call on a nullable union callee,
/// picking the same code tsc would (TS2721 null-only, TS2722 undefined-only, TS2723 both) —
/// unlike member access, calls have no separate bare-identifier code family.
/// </summary>
private static TypeCheckException CannotInvokeNullishError(TypeInfo.Union union, int line)
{
(string code, string subject) = (union.ContainsNull, union.ContainsUndefined) switch
{
(true, false) => ("TS2721", "'null'"),
(false, true) => ("TS2722", "'undefined'"),
_ => ("TS2723", "'null' or 'undefined'"),
};
return new TypeCheckException($"Cannot invoke an object which is possibly {subject}.", line, tsCode: code);
}

/// <summary>
/// Checks a call on an interface with call signatures.
/// Returns the return type of the matching call signature.
Expand Down Expand Up @@ -365,6 +381,34 @@ private bool TryCheckBuiltinCall(Expr.Call call, out TypeInfo result)
{ result = new TypeInfo.Symbol(); return true; }
}

// Handle Symbol.for(key) - returns a shared symbol from the global registry. `Symbol` bare
// resolves to Any (LookupVariable), so without this the call fell through and stayed Any too
// — silently bypassing every arithmetic/comparison operand check a real `symbol` would trip.
if (call.Callee is Expr.Get { Object: Expr.Variable { Name.Lexeme: "Symbol" }, Name.Lexeme: "for" })
{
if (call.Arguments.Count != 1)
{
throw new TypeCheckException("Symbol.for() requires exactly one argument.", tsCode: "TS2554");
}
var argType = CheckExpr(call.Arguments[0]);
if (!IsString(argType) && argType is not TypeInfo.Any)
{
throw new TypeCheckException($"Symbol.for() argument must be a string, got '{argType}'.", tsCode: "TS2345");
}
{ result = new TypeInfo.Symbol(); return true; }
}

// Handle Symbol.keyFor(sym) - returns the key registered for a symbol, or undefined if none.
if (call.Callee is Expr.Get { Object: Expr.Variable { Name.Lexeme: "Symbol" }, Name.Lexeme: "keyFor" })
{
if (call.Arguments.Count != 1)
{
throw new TypeCheckException("Symbol.keyFor() requires exactly one argument.", tsCode: "TS2554");
}
CheckExpr(call.Arguments[0]);
{ result = new TypeInfo.Union([new TypeInfo.String(), new TypeInfo.Undefined()]); return true; }
}

// Handle BigInt() constructor - converts number or string to bigint
if (call.Callee is Expr.Variable bigIntVar && bigIntVar.Name.Lexeme == "BigInt")
{
Expand Down
15 changes: 15 additions & 0 deletions TypeSystem/TypeChecker.Compatibility.Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ type is TypeInfo.String ||
private bool IsBigInt(TypeInfo t) =>
IsTypeOfKind(t, type => type is TypeInfo.BigInt or TypeInfo.BigIntLiteral);

/// <summary>
/// True if <paramref name="t"/> is symbol, or a union with AT LEAST ONE symbol constituent.
/// Deliberately NOT built on the Any-permissive <see cref="IsTypeOfKind"/> (which would make
/// `any` satisfy this too) — operator operand checks use this to decide whether tsc's
/// symbol-specific rejection applies, e.g. `(sym || "") + ""` still gets "The '+' operator
/// cannot be applied to type 'symbol'" even though the left side isn't purely symbol, while
/// `sym + any` must NOT be treated as "both sides are symbol".
/// </summary>
private static bool ContainsSymbolType(TypeInfo t) => t switch
{
TypeInfo.Symbol or TypeInfo.UniqueSymbol => true,
TypeInfo.Union u => u.FlattenedTypes.Any(ContainsSymbolType),
_ => false,
};

/// <summary>
/// Checks if a type is a primitive (not valid as WeakMap key or WeakSet value).
/// </summary>
Expand Down
Loading
Loading