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
8 changes: 0 additions & 8 deletions Jint.Tests.Test262/Test262Harness.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,6 @@
"staging/sm/extensions/quote-string-for-nul-character.js",
"staging/sm/misc/builtin-methods-reject-null-undefined-this.js",

// Remaining one-offs, each its own gap: Iterator.from's next() result validation, the Map
// constructor's per-entry type check, Symbol.keyFor over a cross-realm registry, and a module
// namespace re-exported under a string name.
"staging/sm/Iterator/from/wrap-next-not-object-throws.js",
"staging/sm/Map/iterable.js",
"staging/sm/Symbol/keyFor.js",
"staging/sm/module/module-export-name-star.js",

// === PERMANENT EXCLUSIONS ===
//
// Everything above this banner is debt: a gap somebody is expected to pay down, each entry saying
Expand Down
48 changes: 48 additions & 0 deletions Jint.Tests/Runtime/IteratorHelpersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1239,4 +1239,52 @@ public void TheReceiverOfAHelperKeepsItsOwnToStringTag()

result.Should().Be("""["[object Iterator]","[object Array Iterator]","[object Generator]","[object AsyncIterator]"]""");
}

/// <summary>
/// %WrapForValidIteratorPrototype%.next (https://tc39.es/ecma262/#sec-%wrapforvaliditeratorprototype%.next)
/// is Call(nextMethod, iterator) and nothing else: it neither requires the result to be an Object
/// nor reads `done` off it. Those belong to IteratorNext, performed by whoever consumes the
/// wrapper as an iterator -- which calling .next() on it directly is not.
/// </summary>
[Fact]
public void WrapForValidIteratorNextHandsBackWhateverTheWrappedNextReturned()
{
var engine = new Engine();
var result = engine.Evaluate("""
const wrap = value => Iterator.from({ next: () => value });
const values = [undefined, null, 0, false, 'test', Symbol('')];
JSON.stringify(values.map(v => wrap(v).next() === v));
""").AsString();

result.Should().Be("[true,true,true,true,true,true]");
}

[Fact]
public void WrapForValidIteratorNextDoesNotReadDoneOffTheResult()
{
var engine = new Engine();
var result = engine.Evaluate("""
const reads = [];
const step = { get done() { reads.push('done'); return true; }, get value() { reads.push('value'); return 1; } };
const wrapped = Iterator.from({ next: () => step });
const returned = wrapped.next();
JSON.stringify([returned === step, reads]);
""").AsString();

result.Should().Be("""[true,[]]""");
}

[Fact]
public void ConsumingAWrappedIteratorStillRequiresAnObjectResult()
{
var engine = new Engine();
var result = engine.Evaluate("""
(() => {
try { Iterator.from({ next: () => undefined }).toArray(); return false; }
catch (e) { return e instanceof TypeError; }
})();
""");

result.AsBoolean().Should().BeTrue();
}
}
86 changes: 86 additions & 0 deletions Jint.Tests/Runtime/IteratorReceiverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace Jint.Tests.Runtime;

/// <summary>
/// GetIterator (https://tc39.es/ecma262/#sec-getiterator) resolves @@iterator with GetMethod, which
/// is GetV: the lookup goes through ToObject, but the [[Get]] receiver -- and the this value
/// GetIteratorFromMethod (https://tc39.es/ecma262/#sec-getiteratorfrommethod) then calls it with --
/// is the original value. A primitive therefore reaches a strict-mode @@iterator as a primitive,
/// and only a sloppy-mode one sees the wrapper (because its own this-binding boxes it).
/// </summary>
public class IteratorReceiverTests
{
[Fact]
public void EveryConsumerCallsAPrimitivesIteratorMethodWithThePrimitive()
{
var engine = new Engine();
var result = engine.Evaluate("""
const seen = [];
Object.defineProperty(Number.prototype, Symbol.iterator, {
configurable: true,
value() { 'use strict'; seen.push(typeof this + ':' + this); return [].values(); }
});

new Map(1);
new Set(2);
Array.from(3);
const spread = [...4];
for (const x of 5) { }
const [first] = 6;

JSON.stringify(seen);
""").AsString();

result.Should().Be("""["number:1","number:2","number:3","number:4","number:5","number:6"]""");
}

[Fact]
public void ASloppyIteratorMethodStillSeesTheBoxedReceiver()
{
var engine = new Engine();
var result = engine.Evaluate("""
let seen;
Object.defineProperty(Number.prototype, Symbol.iterator, {
configurable: true,
value: function () { seen = typeof this; return [].values(); }
});
[...7];
seen;
""").AsString();

result.Should().Be("object");
}

[Fact]
public void AnIteratorAccessorAlsoReceivesThePrimitive()
{
var engine = new Engine();
var result = engine.Evaluate("""
let seen;
Object.defineProperty(Boolean.prototype, Symbol.iterator, {
configurable: true,
get() { 'use strict'; seen = typeof this + ':' + this; return () => [].values(); }
});
[...true];
seen;
""").AsString();

result.Should().Be("boolean:true");
}

[Fact]
public void AStringReachesAReplacedIteratorMethodAsAPrimitive()
{
var engine = new Engine();
var result = engine.Evaluate("""
let seen;
Object.defineProperty(String.prototype, Symbol.iterator, {
configurable: true,
value() { 'use strict'; seen = typeof this + ':' + this; return [].values(); }
});
[...'ab'];
seen;
""").AsString();

result.Should().Be("string:ab");
}
}
79 changes: 79 additions & 0 deletions Jint.Tests/Runtime/ModuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1286,4 +1286,83 @@ public void ShouldStillApplyTheTemporalDeadZoneToModuleLevelDeclarations()
Invoking(() => _engine.Modules.Import("my-module")).Should().ThrowExactly<JavaScriptException>()
.Which.Message.Should().Be("Cannot access 'x' before initialization");
}

/// <summary>
/// A ModuleExportName has been an arbitrary StringLiteral since ES2022
/// (https://tc39.es/ecma262/#prod-ModuleExportName), so "*" is an ordinary export name and must
/// not be confused with the namespace-object marker of `import * as ns`.
/// </summary>
[Fact]
public void ShouldImportAndExportArbitraryStringNames()
{
_engine.Modules.Add("provider", """
const x = 'ok';
const y = 'dashed';
export { x as "*", y as "a-b c" };
""");
_engine.Modules.Add("consumer", """
import { "*" as star, "a-b c" as dashed } from 'provider';
export const result = star + '|' + dashed;
""");

var ns = _engine.Modules.Import("consumer");

ns.Get("result").AsString().Should().Be("ok|dashed");
}

[Fact]
public void ShouldExposeAStringExportNameOnTheNamespaceObject()
{
_engine.Modules.Add("provider", "const x = 'ok'; export { x as \"*\" };");

var ns = _engine.Modules.Import("provider");

ns.Get("*").AsString().Should().Be("ok");
}

[Fact]
public void ShouldReExportAStringNamedBindingByName()
{
_engine.Modules.Add("provider", "const x = 'ok'; export { x as \"*\" };");
_engine.Modules.Add("middle", "export { \"*\" as star } from 'provider';");
_engine.Modules.Add("consumer", "import { star } from 'middle'; export const result = star;");

var ns = _engine.Modules.Import("consumer");

ns.Get("result").AsString().Should().Be("ok");
}

[Fact]
public void ShouldReExportAnImportedStringNamedBinding()
{
_engine.Modules.Add("provider", "const x = 'ok'; export { x as \"*\" };");
_engine.Modules.Add("middle", "import { \"*\" as star } from 'provider'; export { star };");
_engine.Modules.Add("consumer", "import { star } from 'middle'; export const result = star;");

var ns = _engine.Modules.Import("consumer");

ns.Get("result").AsString().Should().Be("ok");
}

[Fact]
public void ShouldStillTreatStarSyntaxAsTheNamespaceObject()
{
// The negative control for the three above: the marker cases must keep resolving to a namespace.
_engine.Modules.Add("provider", "export const value = 'ok';");
_engine.Modules.Add("consumer", """
import * as direct from 'provider';
import { value } from 'provider';
export { direct };
export const reexported = value;
""");
_engine.Modules.Add("starred", "export * as ns from 'provider';");
_engine.Modules.Add("all", "export * from 'provider';");

var ns = _engine.Modules.Import("consumer");
ns.Get("direct").Get("value").AsString().Should().Be("ok");
ns.Get("reexported").AsString().Should().Be("ok");

_engine.Modules.Import("starred").Get("ns").Get("value").AsString().Should().Be("ok");
_engine.Modules.Import("all").Get("value").AsString().Should().Be("ok");
}
}
14 changes: 8 additions & 6 deletions Jint.Tests/Runtime/StringIterationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,18 @@ public void IteratingAPrimitiveStringReadsTheIteratorOnce(string consume)
}

/// <summary>
/// Array destructuring reaches the same single read, but sees a wrapper rather than the primitive:
/// <c>HandleArrayPattern</c> performs <c>ToObject</c> on the value before asking for an iterator, so the
/// string lane is never entered at all. node reports <c>this</c> as "string" here. Pinned as it is — the
/// early boxing is a separate deviation from the read count this class is about.
/// Array destructuring reaches the same single read with the same receiver. It used to see a wrapper:
/// <c>HandleArrayPattern</c> performs <c>ToObject</c> for its array fast path and for the null/undefined
/// throw, and then asked <em>that</em> for an iterator, where ArrayBindingPattern's step is
/// <c>GetIterator(value, sync)</c> over the value itself
/// (https://tc39.es/ecma262/#sec-runtime-semantics-bindinginitialization). node reports <c>this</c> as
/// "string", and so does Jint now — which also means the string lane is entered here as it is everywhere else.
/// </summary>
[Fact]
public void DestructuringAPrimitiveStringReadsTheIteratorOnceThroughAWrapper()
public void DestructuringAPrimitiveStringReadsTheIteratorOnce()
{
Probe("String.prototype", "'hi'", "var [a, b] = v; return [a, b];")
.Should().Be("""reads=1 this=object result=["h","i"]""");
.Should().Be("""reads=1 this=string result=["h","i"]""");
}

[Theory]
Expand Down
99 changes: 99 additions & 0 deletions Jint.Tests/Runtime/SymbolRegistryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using Jint.Runtime;

namespace Jint.Tests.Runtime;

/// <summary>
/// The GlobalSymbolRegistry (https://tc39.es/ecma262/#sec-symbol.for) is a list of
/// { [[Key]], [[Symbol]] } records, so both questions asked of it -- KeyForSymbol
/// (https://tc39.es/ecma262/#sec-keyforsymbol) and CanBeHeldWeakly
/// (https://tc39.es/ecma262/#sec-canbeheldweakly) -- are about the identity of a symbol, never
/// about its description. Two symbols may share a description while only one of them is registered.
/// </summary>
public class SymbolRegistryTests
{
[Fact]
public void KeyForAnswersForTheRegisteredSymbolOnly()
{
var engine = new Engine();
var result = engine.Evaluate("""
const registered = Symbol.for('moon');
const plain = Symbol('moon');
JSON.stringify([
Symbol.keyFor(registered),
Symbol.keyFor(plain) === undefined,
Symbol.keyFor(Symbol.iterator) === undefined,
Symbol.keyFor(Symbol.for(''))
]);
""").AsString();

result.Should().Be("""["moon",true,true,""]""");
}

[Fact]
public void KeyForRejectsANonSymbolAndAWrappedSymbol()
{
var engine = new Engine();
var result = engine.Evaluate("""
const threw = f => { try { f(); return false; } catch (e) { return e instanceof TypeError; } };
JSON.stringify([
threw(() => Symbol.keyFor()),
threw(() => Symbol.keyFor(Object(Symbol('moon')))),
Symbol.keyFor.length
]);
""").AsString();

result.Should().Be("[true,true,1]");
}

[Fact]
public void ASymbolSharingADescriptionWithARegisteredOneCanStillBeHeldWeakly()
{
var engine = new Engine();
var result = engine.Evaluate("""
Symbol.for('moon');
const plain = Symbol('moon');
const set = new WeakSet();
set.add(plain);
const map = new WeakMap();
map.set(plain, 1);
new WeakRef(plain);
new FinalizationRegistry(() => { }).register(plain, 'held');
JSON.stringify([set.has(plain), map.get(plain)]);
""").AsString();

result.Should().Be("[true,1]");
}

[Fact]
public void ARegisteredSymbolCannotBeHeldWeakly()
{
var engine = new Engine();
var result = engine.Evaluate("""
const registered = Symbol.for('moon');
const threw = f => { try { f(); return false; } catch (e) { return e instanceof TypeError; } };
JSON.stringify([
threw(() => new WeakSet().add(registered)),
threw(() => new WeakMap().set(registered, 1)),
threw(() => new WeakRef(registered)),
threw(() => new FinalizationRegistry(() => { }).register(registered, 'held'))
]);
""").AsString();

result.Should().Be("[true,true,true,true]");
}

[Fact]
public void SymbolForKeepsHandingBackTheSameSymbolForAKey()
{
var engine = new Engine();
var result = engine.Evaluate("""
JSON.stringify([
Symbol.for('moon') === Symbol.for('moon'),
Symbol.for('moon') === Symbol('moon'),
Symbol.keyFor(Symbol.for('moon'))
]);
""").AsString();

result.Should().Be("""[true,false,"moon"]""");
}
}
4 changes: 2 additions & 2 deletions Jint/AstExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ internal static void GetImportEntries(this ImportDeclaration import, List<Import
switch (specifier)
{
case ImportNamespaceSpecifier namespaceSpecifier:
importEntries.Add(new ImportEntry(moduleRequest, "*", namespaceSpecifier.Local.GetModuleKey(), phase));
importEntries.Add(new ImportEntry(moduleRequest, ImportName: null, namespaceSpecifier.Local.GetModuleKey(), phase, ModuleImportName.Namespace));
break;
case ImportSpecifier importSpecifier:
importEntries.Add(new ImportEntry(moduleRequest, importSpecifier.Imported.GetModuleKey(), importSpecifier.Local.GetModuleKey()!, phase));
Expand Down Expand Up @@ -465,7 +465,7 @@ internal static void GetExportEntries(this ExportDeclaration export, List<Export
case ExportAllDeclaration allDeclaration:
//Note: there is a pending PR for Esprima to support exporting an imported modules content as a namespace i.e. 'export * as ns from "mod"'
requestedModules.Add(new ModuleRequest(allDeclaration.Source.Value, []));
exportEntries.Add(new(allDeclaration.Exported?.GetModuleKey(), new ModuleRequest(allDeclaration.Source.Value, []), "*", null));
exportEntries.Add(new(allDeclaration.Exported?.GetModuleKey(), new ModuleRequest(allDeclaration.Source.Value, []), ImportName: null, LocalName: null, ModuleImportName.Namespace));
break;
case ExportNamedDeclaration namedDeclaration:
ref readonly var specifiers = ref namedDeclaration.Specifiers;
Expand Down
Loading