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
7 changes: 5 additions & 2 deletions SharpTS.Tests/SharedTests/MethodCompletionValueTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,13 @@ class C { inst() {} }
public void OffEndInstanceMethod_InArithmetic_IsNaN(ExecutionMode mode)
{
// undefined + 10 === NaN, but null + 10 === 10 — this distinguishes the
// undefined sentinel from CLR null beyond the typeof check.
// undefined sentinel from CLR null beyond the typeof check. The `as any` is
// required because `inst()`'s return type now correctly infers `void` (#661/#658)
// and `void + 10` is a type error (TS2365, matching tsc); the cast keeps this a
// pure runtime-sentinel probe.
var source = """
class C { inst() {} }
console.log(new C().inst() + 10);
console.log((new C().inst() as any) + 10);
""";
Assert.Equal("NaN\n", TestHarness.Run(source, mode));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,40 @@ public void AsyncGenerator_SelfAssignment_Accepted()
TestHarness.RunInterpreted(source);
}

[Fact]
public void Generator_ThreeTypeArgs_LibSpelling_Accepted()
{
// lib.d.ts is Generator<T, TReturn = any, TNext = unknown>; SharpTS keeps only the yield type but
// must accept (and ignore) the optional TReturn/TNext rather than rejecting "requires exactly 1"
// (#487). This is the issue's exact repro.
var source = """
function* gen(): Generator<number, void, unknown> { yield 1; }
console.log([...gen()]);
""";
TestHarness.RunInterpreted(source);
}

[Fact]
public void Generator_TooManyTypeArgs_Rejected()
{
// Beyond the defaulted range (T, TReturn, TNext) is the range error TS2707, not the exact-count
// TS2314 (#487) — mirrors the Iterator arm.
var source = "function* gen(): Generator<number, void, unknown, string> { yield 1; }";
var ex = Assert.ThrowsAny<TypeCheckException>(() => TestHarness.RunInterpreted(source));
Assert.Equal("TS2707", ex.Diagnostic.TsCode);
}

[Fact]
public void AsyncGenerator_ThreeTypeArgs_LibSpelling_Accepted()
{
// AsyncGenerator<T, TReturn = any, TNext = unknown> — same defaulted range as Generator (#487).
var source = """
async function* gen(): AsyncGenerator<number, void, unknown> { yield 1; }
let g: AsyncGenerator<number> = gen();
""";
TestHarness.RunInterpreted(source);
}

[Fact]
public void Generator_AssignableToIterableIterator_Accepted()
{
Expand Down
106 changes: 101 additions & 5 deletions SharpTS.Tests/TypeCheckerTests/GeneratorYieldInferenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,105 @@ function outer(): number[] {
TestHarness.RunInterpreted(source);
}

// NOTE: generator METHODS with an inferred return type now also compute Generator<yieldType> rather
// than Generator<void> (the same fix in CheckClassBody), but the result is not yet observable at a call
// site: `new C().values()` resolves an inferred method's return to the unresolved `<inferred>`
// placeholder regardless of generator-ness — a pre-existing method-return-inference propagation gap
// (#658). A test here would exercise that unrelated bug, so it is intentionally omitted.
// ---- #661/#658: a class method's inferred return type is now observable at the call site ----
// CheckClassBody computes the inferred (un-annotated) method return type during the body pass and
// re-publishes the frozen class afterwards, so `new C().m()` no longer reads the `<inferred>` placeholder.

[Theory]
[MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))]
public void GeneratorMethod_InferredYield_IsIterable_RunsInBothModes(ExecutionMode mode)
{
// The #661 headline: spreading a generator method with no explicit Generator<T> return type used to
// fail "must be an iterable type ... got '<inferred>'". The inferred Generator<number> is now visible.
var source = """
class C { *m() { yield 1; yield 2; } }
console.log([...new C().m()][0]);
""";
Assert.Equal("1\n", TestHarness.Run(source, mode));
}

[Fact]
public void GeneratorMethod_InferredYield_WrongElementUse_Rejected()
{
// The inferred method element is number, so binding it to string is a genuine TS2322 (not a vacuous
// pass through the old `<inferred>` ~ any placeholder).
var source = """
class C { *m() { yield 1; } }
const s: string = [...new C().m()][0];
""";
var ex = Assert.ThrowsAny<TypeCheckException>(() => TestHarness.RunInterpreted(source));
Assert.Equal("TS2322", ex.Diagnostic.TsCode);
}

[Theory]
[MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))]
public void GeneratorMethod_InferredYield_ForOf_RunsInBothModes(ExecutionMode mode)
{
var source = """
class C { *m() { yield 10; yield 20; } }
let total = 0;
for (const v of new C().m()) { total += v; }
console.log(total);
""";
Assert.Equal("30\n", TestHarness.Run(source, mode));
}

[Theory]
[MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))]
public void AsyncGeneratorMethod_InferredYield_RunsInBothModes(ExecutionMode mode)
{
// The inferred async generator method computes AsyncGenerator<number> (its yield type), so
// `for await...of` binds number and the compiled state machine emits a valid element type.
var source = """
class C { async *m() { yield 1; yield 2; } }
async function run() {
let total = 0;
for await (const v of new C().m()) { total += v; }
console.log(total);
}
run();
""";
Assert.Equal("3\n", TestHarness.Run(source, mode));
}

[Fact]
public void StaticGeneratorMethod_InferredYield_Interpreted()
{
// The inferred return type propagates for a STATIC generator method too (CheckClassBody writes the
// resolved type into StaticMethods). Interpreter-only: the compiler does not yet lower a static
// generator method's body ("Yield not supported in this context"), a pre-existing limitation
// independent of return-type inference — it fails the same way with an explicit return type (#692).
var source = """
class C { static *gen() { yield 7; } }
console.log([...C.gen()][0]);
""";
Assert.Equal("7\n", TestHarness.RunInterpreted(source));
}

[Fact]
public void OrdinaryMethod_InferredReturn_WrongAssignment_Rejected()
{
// #658 (the non-generator face of the same propagation gap): an ordinary inferred method's return
// type now reaches the call site, so a bad assignment is the TS2322 tsc reports rather than a
// vacuous pass through `<inferred>` ~ any.
var source = """
class C { name() { return "hi"; } }
const n: number = new C().name();
""";
var ex = Assert.ThrowsAny<TypeCheckException>(() => TestHarness.RunInterpreted(source));
Assert.Equal("TS2322", ex.Diagnostic.TsCode);
}

[Theory]
[MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))]
public void OrdinaryMethod_InferredReturn_CorrectUse_RunsInBothModes(ExecutionMode mode)
{
// The same propagation must not over-reject: a correctly-typed consumer of the inferred return runs.
var source = """
class C { name() { return "hi"; } }
const s: string = new C().name();
console.log(s);
""";
Assert.Equal("hi\n", TestHarness.Run(source, mode));
}
}
117 changes: 117 additions & 0 deletions SharpTS.Tests/TypeCheckerTests/StructuralIterableTypingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -432,4 +432,121 @@ public void StructuralIterable_IteratesAndSpreads(ExecutionMode mode)
""";
Assert.Equal("30\n3\n", TestHarness.Run(source, mode));
}

// ---- #593: class-instance iterability (plain instance non-iterable; `extends Array` instance iterable) ----
// A class is iterable only via an inherited [Symbol.iterator]. SharpTS previously bound `any` for a
// plain instance in for...of (under-rejecting) and threw a spurious TS2488 for an `extends Array`
// instance in yield* (over-rejecting); both now match tsc.

[Fact]
public void PlainClassInstance_ForOf_RejectedTS2488()
{
// tsc: TS2488 — a plain class instance has no [Symbol.iterator]. SharpTS used to bind `any`.
var source = """
class P { x = 1; }
function f(p: P) { for (const v of p) { console.log(v); } }
""";
var ex = Assert.ThrowsAny<TypeCheckException>(() => TestHarness.RunInterpreted(source));
Assert.Equal("TS2488", ex.Diagnostic.TsCode);
}

[Fact]
public void PlainClassInstance_Spread_RejectedTS2488()
{
var source = """
class P { x = 1; }
function f(p: P) { return [...p]; }
""";
var ex = Assert.ThrowsAny<TypeCheckException>(() => TestHarness.RunInterpreted(source));
Assert.Equal("TS2488", ex.Diagnostic.TsCode);
}

[Fact]
public void PlainClassInstance_YieldStar_RejectedTS2488()
{
var source = """
class P { x = 1; }
function* g(p: P) { yield* p; }
""";
var ex = Assert.ThrowsAny<TypeCheckException>(() => TestHarness.RunInterpreted(source));
Assert.Equal("TS2488", ex.Diagnostic.TsCode);
}

[Fact]
public void ClassWithNextButNoSymbolIterator_ForOf_RejectedTS2488()
{
// Having a next() method does not make a class iterable in tsc (needs [Symbol.iterator]); the
// SharpTS runtime agrees (for...of over such an instance throws), so the static TS2488 matches both.
var source = """
class Counter { next() { return { value: 1, done: false }; } }
function f(c: Counter) { for (const v of c) { console.log(v); } }
""";
var ex = Assert.ThrowsAny<TypeCheckException>(() => TestHarness.RunInterpreted(source));
Assert.Equal("TS2488", ex.Diagnostic.TsCode);
}

[Fact]
public void ExtendsArray_YieldStar_Accepted()
{
// tsc: OK — C inherits Array's [Symbol.iterator]. SharpTS used to throw a spurious TS2488.
var source = """
class C extends Array<number> {}
function* g(x: C) { yield* x; }
""";
TestHarness.RunInterpreted(source);
}

[Theory]
[MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))]
public void ExtendsArray_ForOf_BindsElementType_RunsInBothModes(ExecutionMode mode)
{
// The inherited element type is number (recovered from the dropped `extends Array<number>` arg), and
// the runtime supports iterating an Array subclass.
var source = """
class C extends Array<number> {}
const c = new C();
c.push(10); c.push(20);
let total = 0;
for (const v of c) { total += v; }
console.log(total);
""";
Assert.Equal("30\n", TestHarness.Run(source, mode));
}

[Fact]
public void ExtendsArray_ForOf_ElementIsNumberNotAny_WrongUseRejected()
{
// The bound element is genuinely number (not `any`), so misusing it as string is a TS2322.
var source = """
class C extends Array<number> {}
function f(c: C) { for (const v of c) { const s: string = v; } }
""";
var ex = Assert.ThrowsAny<TypeCheckException>(() => TestHarness.RunInterpreted(source));
Assert.Equal("TS2322", ex.Diagnostic.TsCode);
}

[Fact]
public void ExtendsArray_ThroughUserSubclass_IsIterable()
{
// Iterability is inherited transitively: B → A → Array<number>.
var source = """
class A extends Array<number> {}
class B extends A {}
function f(b: B) { for (const v of b) { const n: number = v; } }
""";
TestHarness.RunInterpreted(source);
}

[Fact]
public void ExtendsNonIterableBuiltin_ForOf_RejectedTS2488()
{
// `extends Error` is not iterable in tsc; the enumeration of iterable bases excludes it, so the
// instance is correctly reported non-iterable rather than binding `any`.
var source = """
class MyErr extends Error {}
function f(e: MyErr) { for (const v of e) {} }
""";
var ex = Assert.ThrowsAny<TypeCheckException>(() => TestHarness.RunInterpreted(source));
Assert.Equal("TS2488", ex.Diagnostic.TsCode);
}
}
46 changes: 43 additions & 3 deletions TypeSystem/TypeChecker.Iterables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,49 @@ private bool TryGetStructuralIterableElement(TypeInfo source, out TypeInfo eleme
TypeInfo? iteratorFactory = GetMemberType(source, "@@iterator");
if (!IsCallableMember(iteratorFactory) && source is TypeInfo.Record { SymbolIndexType: { } symIndex })
iteratorFactory = symIndex; // object-literal [Symbol.iterator]() lands in the symbol index
if (!IsCallableMember(iteratorFactory)) return false;
if (!IsCallableMember(iteratorFactory))
{
// A class instance may inherit iterability from a built-in iterable base
// (`class C extends Array<number>`). That placeholder superclass is a name-only MutableClass
// the normal member walk skips, so probe the hierarchy directly (#593).
return source is TypeInfo.Instance instance
&& TryGetInstanceExtendsBuiltInIterableElement(instance, out elementType);
}

TypeInfo? iterator = GetCallableReturnType(iteratorFactory);
if (iterator is null) { elementType = new TypeInfo.Any(); return true; }

return TryGetIteratorElementFromReturn(iterator, out elementType);
}

/// <summary>
/// Element type of a class instance that (transitively) extends a built-in iterable, read from the
/// <c>@@iterator</c> the <c>extends Array&lt;T&gt;</c>/<c>Set&lt;T&gt;</c>/… placeholder records (see
/// <see cref="ResolveDeclaredSuperclass"/>). That placeholder superclass is a name-only
/// <see cref="TypeInfo.MutableClass"/> which <c>ClassInfoAccessor</c>'s chain walk skips, so the
/// hierarchy is walked directly here — mirroring <c>ExtendsBuiltInError</c>. Returns <c>false</c> for an
/// instance with no built-in-iterable base, leaving it to be reported non-iterable (#593).
/// </summary>
private bool TryGetInstanceExtendsBuiltInIterableElement(TypeInfo.Instance instance, out TypeInfo elementType)
{
elementType = null!;
TypeInfo? current = instance.ResolvedClassType;
for (int guard = 0; current != null && guard < 64; guard++)
{
TypeInfo? iteratorFactory = current is TypeInfo.MutableClass mc
? (mc.Methods.TryGetValue("@@iterator", out var m) ? m : null)
: GetMethods(current)?.GetValueOrDefault("@@iterator");
if (IsCallableMember(iteratorFactory))
{
TypeInfo? iterator = GetCallableReturnType(iteratorFactory);
if (iterator is null) { elementType = new TypeInfo.Any(); return true; }
return TryGetIteratorElementFromReturn(iterator, out elementType);
}
current = GetSuperclass(current);
}
return false;
}

/// <summary>
/// Element of the iterator value returned by <c>[Symbol.iterator]()</c>: a dedicated iterator/generator
/// record carries it directly, otherwise it is read structurally from the iterator's <c>next()</c>. An
Expand Down Expand Up @@ -129,14 +164,19 @@ private bool TryGetIteratorElementFromReturn(TypeInfo iterator, out TypeInfo ele
/// <c>@@iterator</c> member (see TypeChecker.Statements.Interfaces) — so an interface carrying a
/// number index might be iterable-via-Array and is spared to avoid a false TS2488. A genuine
/// <c>[Symbol.iterator]()</c> interface member is caught earlier by the structural probe.</item>
/// <item><b>Class instance</b>: a class is iterable only via an inherited <c>[Symbol.iterator]</c>.
/// An <c>extends Array/Set/Map/...</c> instance carries one (recovered by the structural probe via
/// <see cref="TryGetInstanceExtendsBuiltInIterableElement"/>) and is caught earlier; a class cannot
/// declare its own <c>[Symbol.iterator]()</c> yet (#592), so any instance reaching here is genuinely
/// non-iterable, matching tsc's TS2488 instead of binding <c>any</c> (#593). When #592 lands, a
/// user <c>[Symbol.iterator]</c> becomes a real member the structural probe finds first.</item>
/// </list>
/// Class instances are intentionally excluded: a class may <c>extends Array</c> (iterable) yet that
/// base is a name-only placeholder here, so non-iterability can't be proven — tracked separately.
/// </summary>
private static bool IsProvablyNonIterableStructuralObject(TypeInfo source) => source switch
{
TypeInfo.Record => true,
TypeInfo.Interface { NumberIndexType: null } => true,
TypeInfo.Instance => true,
_ => false
};

Expand Down
Loading
Loading