Skip to content

Commit 92f48f4

Browse files
authored
tools: enforce iterator result property order
Add a custom ESLint rule requiring iterator result objects to place `done` before `value`, and update existing lib iterator result objects to follow the rule. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: #63526 Reviewed-By: Mattias Buelens <mattias@buelens.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent cebe424 commit 92f48f4

9 files changed

Lines changed: 154 additions & 27 deletions

File tree

lib/eslint.config_partial.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ export default [
421421
'node-core/alphabetize-errors': 'error',
422422
'node-core/alphabetize-primordials': 'error',
423423
'node-core/avoid-prototype-pollution': 'error',
424+
'node-core/iterator-result-done-first': 'error',
424425
'node-core/lowercase-name-for-primitive': 'error',
425426
'node-core/non-ascii-character': 'error',
426427
'node-core/no-array-destructuring': 'error',

lib/events.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1032,7 +1032,7 @@ async function once(emitter, name, options = kEmptyObject) {
10321032
}
10331033

10341034
function createIterResult(value, done) {
1035-
return { value, done };
1035+
return { done, value };
10361036
}
10371037

10381038
function eventTargetAgnosticRemoveListener(emitter, name, listener, flags) {

lib/internal/fs/promises.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,7 @@ if (getOptionValue('--experimental-stream-iter')) {
657657
done = true;
658658
cleanup();
659659
}
660-
return { value: undefined, done: true };
660+
return { done: true, value: undefined };
661661
}
662662
const toRead = remaining > 0 ?
663663
MathMin(readSize, remaining) : readSize;
@@ -673,20 +673,20 @@ if (getOptionValue('--experimental-stream-iter')) {
673673
if (bytesRead === 0) {
674674
done = true;
675675
cleanup();
676-
return { value: undefined, done: true };
676+
return { done: true, value: undefined };
677677
}
678678
if (pos >= 0) pos += bytesRead;
679679
if (remaining > 0) remaining -= bytesRead;
680680
const chunk = bytesRead < toRead ?
681681
buf.subarray(0, bytesRead) : buf;
682-
return { value: [chunk], done: false };
682+
return { done: false, value: [chunk] };
683683
},
684684
return() {
685685
if (!done) {
686686
done = true;
687687
cleanup();
688688
}
689-
return { value: undefined, done: true };
689+
return { done: true, value: undefined };
690690
},
691691
};
692692
},

lib/internal/streams/iter/push.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -400,17 +400,17 @@ class PushQueue {
400400
if (this.#writerState === 'closing' && this.#slots.length === 0) {
401401
this.endDrained();
402402
}
403-
return { __proto__: null, value: result, done: false };
403+
return { __proto__: null, done: false, value: result };
404404
}
405405

406406
// Buffer empty and writer closing = drain complete
407407
if (this.#writerState === 'closing') {
408408
this.endDrained();
409-
return { __proto__: null, value: undefined, done: true };
409+
return { __proto__: null, done: true, value: undefined };
410410
}
411411

412412
if (this.#writerState === 'closed') {
413-
return { __proto__: null, value: undefined, done: true };
413+
return { __proto__: null, done: true, value: undefined };
414414
}
415415

416416
if (this.#writerState === 'errored' && this.#error) {
@@ -474,14 +474,14 @@ class PushQueue {
474474
const pending = this.#pendingReads.shift();
475475
const result = this.#drain();
476476
this.#resolvePendingWrites();
477-
pending.resolve({ __proto__: null, value: result, done: false });
477+
pending.resolve({ __proto__: null, done: false, value: result });
478478
} else if (this.#writerState === 'closing' && this.#slots.length === 0) {
479479
this.endDrained();
480480
const pending = this.#pendingReads.shift();
481-
pending.resolve({ __proto__: null, value: undefined, done: true });
481+
pending.resolve({ __proto__: null, done: true, value: undefined });
482482
} else if (this.#writerState === 'closed') {
483483
const pending = this.#pendingReads.shift();
484-
pending.resolve({ __proto__: null, value: undefined, done: true });
484+
pending.resolve({ __proto__: null, done: true, value: undefined });
485485
} else if (this.#writerState === 'errored' && this.#error) {
486486
const pending = this.#pendingReads.shift();
487487
pending.reject(this.#error);
@@ -694,11 +694,11 @@ function createReadable(queue) {
694694
},
695695
async return() {
696696
queue.consumerReturn();
697-
return { __proto__: null, value: undefined, done: true };
697+
return { __proto__: null, done: true, value: undefined };
698698
},
699699
async throw(error) {
700700
queue.consumerThrow(error);
701-
return { __proto__: null, value: undefined, done: true };
701+
return { __proto__: null, done: true, value: undefined };
702702
},
703703
};
704704
},

lib/internal/url.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,8 @@ class URLSearchParamsIterator {
242242
const len = values.length;
243243
if (index >= len) {
244244
return {
245-
value: undefined,
246245
done: true,
246+
value: undefined,
247247
};
248248
}
249249

@@ -261,8 +261,8 @@ class URLSearchParamsIterator {
261261
}
262262

263263
return {
264-
value: result,
265264
done: false,
265+
value: result,
266266
};
267267
}
268268

lib/internal/vfs/watcher.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ class VFSWatchAsyncIterable {
599599
const event = { eventType, filename };
600600
if (this.#pendingResolvers.length > 0) {
601601
const { resolve } = this.#pendingResolvers.shift();
602-
resolve({ value: event, done: false });
602+
resolve({ done: false, value: event });
603603
} else if (this.#pendingEvents.length < kMaxPendingEvents) {
604604
ArrayPrototypePush(this.#pendingEvents, event);
605605
}
@@ -611,7 +611,7 @@ class VFSWatchAsyncIterable {
611611
// Resolve any pending iterators
612612
while (this.#pendingResolvers.length > 0) {
613613
const { resolve } = this.#pendingResolvers.shift();
614-
resolve({ value: undefined, done: true });
614+
resolve({ done: true, value: undefined });
615615
}
616616
});
617617

@@ -648,12 +648,12 @@ class VFSWatchAsyncIterable {
648648
*/
649649
next() {
650650
if (this.#closed) {
651-
return PromiseResolve({ value: undefined, done: true });
651+
return PromiseResolve({ done: true, value: undefined });
652652
}
653653

654654
if (this.#pendingEvents.length > 0) {
655655
const event = this.#pendingEvents.shift();
656-
return PromiseResolve({ value: event, done: false });
656+
return PromiseResolve({ done: false, value: event });
657657
}
658658

659659
return new Promise((resolve, reject) => {
@@ -667,7 +667,7 @@ class VFSWatchAsyncIterable {
667667
*/
668668
return() {
669669
this.#watcher.close();
670-
return PromiseResolve({ value: undefined, done: true });
670+
return PromiseResolve({ done: true, value: undefined });
671671
}
672672

673673
/**
@@ -677,7 +677,7 @@ class VFSWatchAsyncIterable {
677677
*/
678678
throw(error) {
679679
this.#watcher.close();
680-
return PromiseResolve({ value: undefined, done: true });
680+
return PromiseResolve({ done: true, value: undefined });
681681
}
682682
}
683683

lib/internal/webstreams/readablestream.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,7 @@ class ReadableStreamAsyncIteratorReadRequest {
761761

762762
[kChunk](chunk) {
763763
this.state.current = undefined;
764-
this.promise.resolve({ value: chunk, done: false });
764+
this.promise.resolve({ done: false, value: chunk });
765765
}
766766

767767
[kClose]() {
@@ -785,11 +785,11 @@ class DefaultReadRequest {
785785
}
786786

787787
[kChunk](value) {
788-
this[kState].resolve?.({ value, done: false });
788+
this[kState].resolve?.({ done: false, value });
789789
}
790790

791791
[kClose]() {
792-
this[kState].resolve?.({ value: undefined, done: true });
792+
this[kState].resolve?.({ done: true, value: undefined });
793793
}
794794

795795
[kError](error) {
@@ -805,11 +805,11 @@ class ReadIntoRequest {
805805
}
806806

807807
[kChunk](value) {
808-
this[kState].resolve?.({ value, done: false });
808+
this[kState].resolve?.({ done: false, value });
809809
}
810810

811811
[kClose](value) {
812-
this[kState].resolve?.({ value, done: true });
812+
this[kState].resolve?.({ done: true, value });
813813
}
814814

815815
[kError](error) {
@@ -875,7 +875,7 @@ class ReadableStreamDefaultReader {
875875
readableStreamDefaultControllerCallPullIfNeeded(controller);
876876
}
877877

878-
return PromiseResolve({ value: chunk, done: false });
878+
return PromiseResolve({ done: false, value: chunk });
879879
}
880880

881881
// Slow path: create request and go through normal flow
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if ((!common.hasCrypto) || (!common.hasIntl)) {
5+
common.skip('ESLint tests require crypto and Intl');
6+
}
7+
8+
common.skipIfEslintMissing();
9+
10+
const { RuleTester } = require('../../tools/eslint/node_modules/eslint');
11+
const rule = require('../../tools/eslint-rules/iterator-result-done-first');
12+
13+
const message = 'Iterator result objects should place `done` before `value`.';
14+
15+
new RuleTester().run('iterator-result-done-first', rule, {
16+
valid: [
17+
'function next() { return { done: true, value: undefined }; }',
18+
'function next() { return { __proto__: null, done: false, value: chunk }; }',
19+
'function next() { return { done, value }; }',
20+
'function next() { return { value }; }',
21+
'function next() { return { done }; }',
22+
'function next() { return { value: 1, other: 2 }; }',
23+
'function next() { return { [value]: 1, done: true }; }',
24+
'function next() { return { value: 1, [done]: true }; }',
25+
'function next() { return { "done": true, "value": undefined }; }',
26+
'function next() { return { ["done"]: true, ["value"]: undefined }; }',
27+
],
28+
invalid: [
29+
{
30+
code: 'function next() { return { value: undefined, done: true }; }',
31+
errors: [{ message }],
32+
output: 'function next() { return { done: true, value: undefined }; }',
33+
},
34+
{
35+
code: 'function next() { return { __proto__: null, value: chunk, done: false }; }',
36+
errors: [{ message }],
37+
output: 'function next() { return { __proto__: null, done: false, value: chunk }; }',
38+
},
39+
{
40+
code: 'function next() { return { value, done }; }',
41+
errors: [{ message }],
42+
output: 'function next() { return { done, value }; }',
43+
},
44+
{
45+
code: 'function next() { return { "value": undefined, "done": true }; }',
46+
errors: [{ message }],
47+
output: 'function next() { return { "done": true, "value": undefined }; }',
48+
},
49+
{
50+
code: 'function next() { return { ["value"]: undefined, ["done"]: true }; }',
51+
errors: [{ message }],
52+
output: 'function next() { return { ["done"]: true, ["value"]: undefined }; }',
53+
},
54+
{
55+
code: 'function next() { return { value: result, extra: true, done: false }; }',
56+
errors: [{ message }],
57+
output: 'function next() { return { done: false, extra: true, value: result }; }',
58+
},
59+
],
60+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use strict';
2+
3+
const MESSAGE = 'Iterator result objects should place `done` before `value`.';
4+
5+
function getStaticPropertyName(property) {
6+
const { key } = property;
7+
8+
if (!key) {
9+
return;
10+
}
11+
12+
if (key.type === 'Identifier' && !property.computed) {
13+
return key.name;
14+
}
15+
16+
if (key.type === 'Literal') {
17+
return key.value;
18+
}
19+
}
20+
21+
module.exports = {
22+
meta: {
23+
type: 'suggestion',
24+
fixable: 'code',
25+
schema: [],
26+
},
27+
28+
create(context) {
29+
const sourceCode = context.sourceCode;
30+
31+
return {
32+
ObjectExpression(node) {
33+
let doneProperty;
34+
let valueProperty;
35+
36+
for (const property of node.properties) {
37+
if (property.type !== 'Property') {
38+
continue;
39+
}
40+
41+
switch (getStaticPropertyName(property)) {
42+
case 'done':
43+
doneProperty ??= property;
44+
break;
45+
case 'value':
46+
valueProperty ??= property;
47+
break;
48+
}
49+
}
50+
51+
if (doneProperty && valueProperty && valueProperty.range[0] < doneProperty.range[0]) {
52+
context.report({
53+
node: valueProperty,
54+
message: MESSAGE,
55+
fix(fixer) {
56+
return [
57+
fixer.replaceText(valueProperty, sourceCode.getText(doneProperty)),
58+
fixer.replaceText(doneProperty, sourceCode.getText(valueProperty)),
59+
];
60+
},
61+
});
62+
}
63+
},
64+
};
65+
},
66+
};

0 commit comments

Comments
 (0)