Skip to content

Commit 55e3813

Browse files
committed
Non-enumerable property fails to shadow inherited enumerable property from for-in
https://bugs.webkit.org/show_bug.cgi?id=38970 Reviewed by Keith Miller. JSTests: * stress/arguments-bizarre-behaviour-disable-enumerability.js: * stress/for-in-redefine-enumerable.js: Added. * stress/for-in-shadow-non-enumerable.js: Added. * test262/expectations.yaml: Mark 4 test cases as passing. Source/JavaScriptCore: While for/in was initially specified with notion of "shadowing", it wasn't clarified until ES5 that [[Enumerable]] attributes are ignored when determining if a property has already been processed. Recently, for/in spec was expanded [1] to pin down common case enumeration as it's currently implemented by V8 and SpiderMonkey. Since keeping track of DontEnum properties is a massive slowdown for uncached runs (with any data structure used), this patch simply adds [[Enumerable]] check to has_{indexed,structure,generic}_property bytecode ops and does renaming chores. Common code is now shared between HasIndexedProperty (emitted for `0 in arr`) and HasEnumerableIndexedProperty DFG nodes via passing different slow path ops rather than having OpInfo with PropertySlot::InternalMethodType, which is a nice refactor. While this change aligns common case for/in enumeration with the spec and other engines, it also introduces a few observable discrepancies from V8 and SpiderMonkey, which are permitted by the spec [2]: a) properties that have been redefined as DontEnum within loop body are skipped, which matches the spec [3] and seems like expected behavior; b) "shadowing" is broken if a DontEnum property of already visited object is added / deleted / redefined within loop body, which (pretty much) never happens. This patch introduces a new invariant: all properties getOwn*PropertyNames() returns in DontEnumPropertiesMode::Exclude should be reported as [[Enumerable]] by getOwnPropertySlot(). JSCallbackObject and RuntimeArray are fixed to follow it. for/in and Object.keys microbenchmarks are neutral. This change does not affect JSPropertyNameEnumerator caching, nor fast paths of its bytecodes. [1]: tc39/ecma262#1791 [2]: https://tc39.es/ecma262/#sec-enumerate-object-properties (last paragraph) [3]: https://tc39.es/ecma262/#sec-%foriniteratorprototype%.next (step 7.b.iii) * API/JSCallbackObjectFunctions.h: (JSC::JSCallbackObject<Parent>::getOwnPropertySlot): * API/tests/testapi.c: * API/tests/testapiScripts/testapi.js: * bytecode/BytecodeList.rb: * bytecode/BytecodeUseDef.cpp: (JSC::computeUsesForBytecodeIndexImpl): (JSC::computeDefsForBytecodeIndexImpl): * bytecode/CodeBlock.cpp: (JSC::CodeBlock::finishCreation): * bytecode/Opcode.h: * bytecompiler/BytecodeGenerator.cpp: (JSC::BytecodeGenerator::emitHasEnumerableIndexedProperty): (JSC::BytecodeGenerator::emitHasEnumerableStructureProperty): (JSC::BytecodeGenerator::emitHasEnumerableProperty): (JSC::BytecodeGenerator::emitHasGenericProperty): Deleted. (JSC::BytecodeGenerator::emitHasIndexedProperty): Deleted. (JSC::BytecodeGenerator::emitHasStructureProperty): Deleted. * bytecompiler/BytecodeGenerator.h: * bytecompiler/NodesCodegen.cpp: (JSC::ForInNode::emitBytecode): * dfg/DFGAbstractInterpreterInlines.h: (JSC::DFG::AbstractInterpreter<AbstractStateType>::executeEffects): * dfg/DFGByteCodeParser.cpp: (JSC::DFG::ByteCodeParser::parseBlock): * dfg/DFGCapabilities.cpp: (JSC::DFG::capabilityLevel): * dfg/DFGClobberize.h: (JSC::DFG::clobberize): * dfg/DFGDoesGC.cpp: (JSC::DFG::doesGC): * dfg/DFGFixupPhase.cpp: (JSC::DFG::FixupPhase::fixupNode): (JSC::DFG::FixupPhase::convertToHasIndexedProperty): * dfg/DFGNode.h: (JSC::DFG::Node::hasArrayMode): (JSC::DFG::Node::hasInternalMethodType const): Deleted. (JSC::DFG::Node::internalMethodType const): Deleted. (JSC::DFG::Node::setInternalMethodType): Deleted. * dfg/DFGNodeType.h: * dfg/DFGOperations.cpp: (JSC::DFG::JSC_DEFINE_JIT_OPERATION): * dfg/DFGOperations.h: * dfg/DFGPredictionPropagationPhase.cpp: * dfg/DFGSSALoweringPhase.cpp: (JSC::DFG::SSALoweringPhase::handleNode): * dfg/DFGSafeToExecute.h: (JSC::DFG::safeToExecute): * dfg/DFGSpeculativeJIT.cpp: (JSC::DFG::SpeculativeJIT::compileHasEnumerableProperty): (JSC::DFG::SpeculativeJIT::compileHasEnumerableStructureProperty): (JSC::DFG::SpeculativeJIT::compileHasIndexedProperty): (JSC::DFG::SpeculativeJIT::compileHasGenericProperty): Deleted. (JSC::DFG::SpeculativeJIT::compileHasStructureProperty): Deleted. * dfg/DFGSpeculativeJIT.h: * dfg/DFGSpeculativeJIT32_64.cpp: (JSC::DFG::SpeculativeJIT::compile): * dfg/DFGSpeculativeJIT64.cpp: (JSC::DFG::SpeculativeJIT::compile): * ftl/FTLCapabilities.cpp: (JSC::FTL::canCompile): * ftl/FTLLowerDFGToB3.cpp: (JSC::FTL::DFG::LowerDFGToB3::compileNode): (JSC::FTL::DFG::LowerDFGToB3::compileHasIndexedProperty): (JSC::FTL::DFG::LowerDFGToB3::compileHasEnumerableProperty): (JSC::FTL::DFG::LowerDFGToB3::compileHasEnumerableStructureProperty): (JSC::FTL::DFG::LowerDFGToB3::compileHasGenericProperty): Deleted. (JSC::FTL::DFG::LowerDFGToB3::compileHasStructureProperty): Deleted. * jit/JIT.cpp: (JSC::JIT::privateCompileMainPass): (JSC::JIT::privateCompileSlowCases): * jit/JIT.h: * jit/JITOpcodes.cpp: (JSC::JIT::emit_op_has_enumerable_structure_property): (JSC::JIT::emit_op_has_enumerable_indexed_property): (JSC::JIT::emitSlow_op_has_enumerable_indexed_property): (JSC::JIT::emit_op_has_structure_property): Deleted. (JSC::JIT::emit_op_has_indexed_property): Deleted. (JSC::JIT::emitSlow_op_has_indexed_property): Deleted. * jit/JITOpcodes32_64.cpp: (JSC::JIT::emit_op_has_enumerable_structure_property): (JSC::JIT::emit_op_has_enumerable_indexed_property): (JSC::JIT::emitSlow_op_has_enumerable_indexed_property): (JSC::JIT::emit_op_has_structure_property): Deleted. (JSC::JIT::emit_op_has_indexed_property): Deleted. (JSC::JIT::emitSlow_op_has_indexed_property): Deleted. * jit/JITOperations.cpp: (JSC::JSC_DEFINE_JIT_OPERATION): * jit/JITOperations.h: * llint/LowLevelInterpreter.asm: * llint/LowLevelInterpreter64.asm: * runtime/CommonSlowPaths.cpp: (JSC::JSC_DEFINE_COMMON_SLOW_PATH): * runtime/CommonSlowPaths.h: * runtime/JSObject.cpp: (JSC::JSObject::hasProperty const): (JSC::JSObject::hasEnumerableProperty const): (JSC::JSObject::hasPropertyGeneric const): Deleted. * runtime/JSObject.h: Source/WebCore: Report RuntimeArray indices as [[Enumerable]]. Test: platform/mac/fast/dom/wrapper-classes-objc.html * bridge/runtime_array.cpp: (JSC::RuntimeArray::getOwnPropertySlot): (JSC::RuntimeArray::getOwnPropertySlotByIndex): LayoutTests: * platform/mac/fast/dom/wrapper-classes-objc-expected.txt: * platform/mac/fast/dom/wrapper-classes-objc.html: Canonical link: https://commits.webkit.org/232510@main git-svn-id: https://svn.webkit.org/repository/webkit/trunk@270874 268f45cc-cd09-0410-ab3c-d52691b4dbfc
1 parent 2679e9c commit 55e3813

52 files changed

Lines changed: 693 additions & 197 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎JSTests/ChangeLog‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
2020-12-15 Alexey Shvayka <shvaikalesh@gmail.com>
2+
3+
Non-enumerable property fails to shadow inherited enumerable property from for-in
4+
https://bugs.webkit.org/show_bug.cgi?id=38970
5+
6+
Reviewed by Keith Miller.
7+
8+
* stress/arguments-bizarre-behaviour-disable-enumerability.js:
9+
* stress/for-in-redefine-enumerable.js: Added.
10+
* stress/for-in-shadow-non-enumerable.js: Added.
11+
* test262/expectations.yaml: Mark 4 test cases as passing.
12+
113
2020-12-15 Yusuke Suzuki <ysuzuki@apple.com>
214

315
Suppress noise from JSTests/stress/lars-sab-workers.js

‎JSTests/stress/arguments-bizarre-behaviour-disable-enumerability.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var array = [];
1818
for (var s in result[2])
1919
array.push(s);
2020

21-
if (array.join(",") != "0")
21+
if (array.join(",") != "")
2222
throw new Error();
2323

2424
if (Object.keys(result[2]).join(",") != "0")
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
function assert(x) {
2+
if (!x)
3+
throw new Error("Bad assertion!");
4+
}
5+
6+
function shouldBe(actual, expected) {
7+
if (actual !== expected)
8+
throw new Error(`Bad value: ${actual}.`);
9+
}
10+
11+
const enumDesc = { value: 0, writable: true, enumerable: true, configurable: true };
12+
const dontEnumDesc = { value: 0, writable: true, enumerable: false, configurable: true };
13+
14+
// indexed property
15+
(() => {
16+
function test() {
17+
var arr = Object.defineProperties([0, 0, 0], { 1: dontEnumDesc });
18+
for (var i in arr) {
19+
assert(i in arr);
20+
shouldBe(arr[i], 0);
21+
++arr[i];
22+
if (i === "0")
23+
Object.defineProperties(arr, { 1: enumDesc, 2: dontEnumDesc });
24+
}
25+
shouldBe(arr[0], 1);
26+
shouldBe(arr[1], 0);
27+
shouldBe(arr[2], 0);
28+
}
29+
30+
for (var i = 0; i < 1e5; ++i)
31+
test();
32+
})();
33+
34+
// structure property
35+
(() => {
36+
function test() {
37+
var obj = Object.create(null, { a: enumDesc, b: enumDesc, c: dontEnumDesc });
38+
for (var key in obj) {
39+
assert(key in obj);
40+
shouldBe(obj[key], 0);
41+
++obj[key];
42+
if (key === "a")
43+
Object.defineProperties(obj, { b: dontEnumDesc, c: enumDesc });
44+
}
45+
shouldBe(obj.a, 1);
46+
shouldBe(obj.b, 0);
47+
shouldBe(obj.c, 0);
48+
}
49+
50+
for (var i = 0; i < 1e5; ++i)
51+
test();
52+
})();
53+
54+
// generic property (Proxy)
55+
(() => {
56+
function test() {
57+
var target = { a: 0, b: 0, c: 0 };
58+
var enumMap = { a: true, b: true, c: false };
59+
var proxy = new Proxy(target, {
60+
getOwnPropertyDescriptor: (_, key) => {
61+
return { value: target[key], writable: true, enumerable: enumMap[key], configurable: true };
62+
},
63+
});
64+
65+
for (var key in proxy) {
66+
assert(key in proxy);
67+
shouldBe(proxy[key], 0);
68+
++target[key];
69+
if (key === "a") {
70+
enumMap.b = false;
71+
enumMap.c = true;
72+
}
73+
}
74+
shouldBe(target.a, 1);
75+
shouldBe(target.b, 0);
76+
shouldBe(target.c, 0);
77+
}
78+
79+
for (var i = 0; i < 1e5; ++i)
80+
test();
81+
})();
82+
83+
// generic property (in prototype)
84+
(() => {
85+
function test() {
86+
var seen = {};
87+
var proto = Object.create(null, { b: enumDesc, c: dontEnumDesc, d: enumDesc, e: enumDesc });
88+
var heir = Object.create(proto, { a: enumDesc, e: dontEnumDesc });
89+
for (var key in heir) {
90+
assert(key in heir);
91+
shouldBe(heir[key], 0);
92+
seen[key] = true;
93+
if (key === "a")
94+
Object.defineProperties(proto, { b: dontEnumDesc, c: enumDesc });
95+
if (key === "d")
96+
Object.defineProperties(heir, { e: enumDesc });
97+
}
98+
assert(seen.a);
99+
assert(!seen.b);
100+
assert(!seen.c);
101+
assert(seen.d);
102+
assert(seen.e);
103+
}
104+
105+
for (var i = 0; i < 1e5; ++i)
106+
test();
107+
})();
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
const dontEnumDesc = { value: 1, writable: true, enumerable: false, configurable: true };
2+
const testCases = [
3+
{
4+
name: "Object",
5+
createObject: () => Object.create(null, { foo: dontEnumDesc, bar: dontEnumDesc }),
6+
dontEnumKeys: ["foo", "bar"],
7+
},
8+
{
9+
name: "Error",
10+
createObject: () => new Error(),
11+
dontEnumKeys: ["line", "column", "sourceURL", "stack"],
12+
},
13+
{
14+
name: "Array (empty)",
15+
createObject: () => [],
16+
dontEnumKeys: ["length"],
17+
},
18+
{
19+
name: "Array (sparse)",
20+
createObject: () => Object.defineProperties([0, 1, 2], { 0: dontEnumDesc, 2: dontEnumDesc }),
21+
dontEnumKeys: ["0", "2", "length"],
22+
},
23+
{
24+
name: "Function (strict)",
25+
createObject: () => function() { "use strict"; },
26+
dontEnumKeys: ["length", "name", "prototype"],
27+
},
28+
{
29+
name: "Function (non-strict)",
30+
createObject: () => function() {},
31+
dontEnumKeys: ["arguments", "caller", "length", "name", "prototype"],
32+
},
33+
{
34+
name: "RegExp",
35+
createObject: () => /(?:)/g,
36+
dontEnumKeys: ["lastIndex"],
37+
},
38+
{
39+
name: "String",
40+
createObject: () => new String("foo"),
41+
dontEnumKeys: ["length"],
42+
},
43+
{
44+
name: "Arguments (strict)",
45+
createObject: function(foo) { "use strict"; return arguments; },
46+
dontEnumKeys: ["length", "callee"],
47+
},
48+
{
49+
name: "Arguments (non-strict)",
50+
createObject: function(foo) { return arguments; },
51+
dontEnumKeys: ["length", "callee"],
52+
},
53+
{
54+
name: "Reflect",
55+
createObject: () => $vm.createGlobalObject().Reflect,
56+
dontEnumKeys: ["apply", "get", "has", "set"],
57+
},
58+
{
59+
name: "Date.prototype",
60+
createObject: () => $vm.createGlobalObject().Date.prototype,
61+
dontEnumKeys: ["toISOString", "getTime", "setYear"],
62+
},
63+
];
64+
65+
// basic tests
66+
for (const t of testCases) {
67+
assert(!contains(forIn(t.createObject()), t.dontEnumKeys), t.name);
68+
assert(!contains(Object.keys(t.createObject()), t.dontEnumKeys), t.name);
69+
70+
assert(contains(Object.getOwnPropertyNames(t.createObject()), t.dontEnumKeys), t.name);
71+
assert(contains(Reflect.ownKeys(t.createObject()), t.dontEnumKeys), t.name);
72+
}
73+
74+
// shadowing: DontEnum => Enum
75+
for (const t of testCases) {
76+
assert(
77+
!contains(
78+
forIn(makePrototypeChain(t.createObject(), makeObject(t.dontEnumKeys))),
79+
t.dontEnumKeys,
80+
),
81+
t.name,
82+
);
83+
}
84+
85+
// shadowing: {} => DontEnum => {} => Enum
86+
for (const t of testCases) {
87+
assert(
88+
!contains(
89+
forIn(makePrototypeChain({}, t.createObject(), {}, makeObject(t.dontEnumKeys))),
90+
t.dontEnumKeys,
91+
),
92+
t.name,
93+
);
94+
}
95+
96+
// shadowing: DontEnum => {} => Enum => {} => Enum
97+
for (const t of testCases) {
98+
assert(
99+
!contains(
100+
forIn(makePrototypeChain(t.createObject(), {}, makeObject(t.dontEnumKeys), {}, makeObject(t.dontEnumKeys))),
101+
t.dontEnumKeys,
102+
),
103+
t.name,
104+
);
105+
}
106+
107+
// shadowing: {} => DontEnum (enumerable: true => false) => {} => Enum
108+
for (const t of testCases) {
109+
const dontEnumObject = t.createObject();
110+
const target = makePrototypeChain({}, dontEnumObject, {}, makeObject(t.dontEnumKeys));
111+
assert(!contains(forIn(target), t.dontEnumKeys), t.name);
112+
113+
const enumKeys = t.dontEnumKeys.filter(key => Reflect.defineProperty(dontEnumObject, key, { enumerable: true }));
114+
assert(contains(forIn(target), enumKeys), t.name);
115+
}
116+
117+
// shadowing: {} => DontEnum (delete non-enumerable keys) => {} => Enum
118+
for (const t of testCases) {
119+
const dontEnumObject = t.createObject();
120+
const target = makePrototypeChain({}, dontEnumObject, {}, makeObject(t.dontEnumKeys));
121+
assert(!contains(forIn(target), t.dontEnumKeys), t.name);
122+
123+
const enumKeys = t.dontEnumKeys.filter(key => Reflect.deleteProperty(dontEnumObject, key));
124+
assert(contains(forIn(target), enumKeys), t.name);
125+
}
126+
127+
// shadowing: {} => DontEnum (materialized) => {} => Enum
128+
for (const t of testCases) {
129+
const dontEnumObject = t.createObject();
130+
const target = makePrototypeChain({}, dontEnumObject, {}, makeObject(t.dontEnumKeys));
131+
assert(!contains(forIn(target), t.dontEnumKeys), t.name);
132+
133+
const dontEnumKeys = t.dontEnumKeys.filter(key => {
134+
const desc = Object.getOwnPropertyDescriptor(dontEnumObject, key);
135+
if (!Reflect.deleteProperty(dontEnumObject, key)) return false;
136+
Object.defineProperty(dontEnumObject, key, desc);
137+
return true;
138+
});
139+
140+
if (dontEnumKeys.length)
141+
assert(!contains(forIn(target), dontEnumKeys), t.name);
142+
}
143+
144+
// helpers
145+
function assert(value, name) {
146+
if (!value)
147+
throw new Error(`Bad value: ${value}. Test case: ${name}.`);
148+
}
149+
150+
function contains(array, subarray) {
151+
return subarray.every(item => array.includes(item));
152+
}
153+
154+
function forIn(object) {
155+
const keys = [];
156+
for (const key in object)
157+
keys.push(key);
158+
return keys;
159+
}
160+
161+
function makePrototypeChain(...objects) {
162+
objects.reduce((object, prototype) => {
163+
Object.setPrototypeOf(object, prototype);
164+
return prototype;
165+
});
166+
return objects[0];
167+
}
168+
169+
function makeObject(keys) {
170+
const object = {};
171+
for (const key of keys)
172+
object[key] = key;
173+
return object;
174+
}

‎JSTests/test262/expectations.yaml‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1858,16 +1858,10 @@ test/language/statements/for-await-of/ticks-with-async-iter-resolved-promise-and
18581858
test/language/statements/for-await-of/ticks-with-sync-iter-resolved-promise-and-constructor-lookup.js:
18591859
default: 'Test262:AsyncTestFailure:Test262Error: Test262Error: Expected [pre, tick 1, constructor, constructor, tick 2, tick 3, loop, tick 4, constructor] and [pre, constructor, constructor, tick 1, tick 2, loop, constructor, tick 3, tick 4, post] to have the same contents. Ticks and constructor lookups'
18601860
strict mode: 'Test262:AsyncTestFailure:Test262Error: Test262Error: Expected [pre, tick 1, constructor, constructor, tick 2, tick 3, loop, tick 4, constructor] and [pre, constructor, constructor, tick 1, tick 2, loop, constructor, tick 3, tick 4, post] to have the same contents. Ticks and constructor lookups'
1861-
test/language/statements/for-in/12.6.4-2.js:
1862-
default: 'Test262Error: accessedProp Expected SameValue(«true», «false») to be true'
1863-
strict mode: 'Test262Error: accessedProp Expected SameValue(«true», «false») to be true'
18641861
test/language/statements/for-in/head-lhs-let.js:
18651862
default: "SyntaxError: Cannot use the keyword 'in' as a lexical variable name."
18661863
test/language/statements/for-in/identifier-let-allowed-as-lefthandside-expression-not-strict.js:
18671864
default: "SyntaxError: Cannot use the keyword 'in' as a lexical variable name."
1868-
test/language/statements/for-in/order-enumerable-shadowed.js:
1869-
default: 'Test262Error: Expected [p1, p2] and [p1] to have the same contents. '
1870-
strict mode: 'Test262Error: Expected [p1, p2] and [p1] to have the same contents. '
18711865
test/language/statements/for-in/scope-body-lex-open.js:
18721866
default: 'Test262Error: Expected a ReferenceError to be thrown but no exception was thrown at all'
18731867
strict mode: 'Test262Error: Expected a ReferenceError to be thrown but no exception was thrown at all'

‎LayoutTests/ChangeLog‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
2020-12-15 Alexey Shvayka <shvaikalesh@gmail.com>
2+
3+
Non-enumerable property fails to shadow inherited enumerable property from for-in
4+
https://bugs.webkit.org/show_bug.cgi?id=38970
5+
6+
Reviewed by Keith Miller.
7+
8+
* platform/mac/fast/dom/wrapper-classes-objc-expected.txt:
9+
* platform/mac/fast/dom/wrapper-classes-objc.html:
10+
111
2020-12-15 Alex Christensen <achristensen@webkit.org>
212

313
REGRESSION: [macOS] http/tests/inspector/network/resource-response-service-worker.html is a flaky failure

‎LayoutTests/platform/mac/fast/dom/wrapper-classes-objc-expected.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ PASS typeof objCObjectOfClass('NSCFString') is 'string'
189189
PASS typeof objCObjectOfClass('WebScriptObject') is 'object'
190190
PASS objCObjectOfClass('NSArray') instanceof Array is true
191191
PASS concatenateArray(objCArrayOfString()) is 'onetwothree'
192+
PASS objCArrayOfString().every((_, i, arr) => arr.propertyIsEnumerable(i)) is true
192193
PASS let arr = objCArrayOfString(); arr.length is 3
193194
PASS let arr = objCArrayOfString(); arr.length = 0 threw exception RangeError: Range error.
194195
PASS let arr = objCArrayOfString(); arr.length = 5 threw exception RangeError: Range error.

‎LayoutTests/platform/mac/fast/dom/wrapper-classes-objc.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@
286286
shouldBeTrue("objCObjectOfClass('NSArray') instanceof Array");
287287

288288
shouldBe("concatenateArray(objCArrayOfString())", "'onetwothree'");
289+
shouldBeTrue("objCArrayOfString().every((_, i, arr) => arr.propertyIsEnumerable(i))");
289290

290291
shouldBe("let arr = objCArrayOfString(); arr.length", "3");
291292
shouldThrow("let arr = objCArrayOfString(); arr.length = 0");

0 commit comments

Comments
 (0)