lib: add lint rules for reflective function calls

PR-URL: https://github.com/nodejs/node/pull/60825
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Reviewed-By: LiviaMedeiros <livia@cirno.name>
This commit is contained in:
Antoine du Hamel 2025-11-27 10:24:09 +01:00 committed by GitHub
parent 0177491df2
commit 0392f0caea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 32 additions and 20 deletions

View File

@ -672,8 +672,9 @@ assignFunctionName(EE.captureRejectionSymbol, function(err, event, ...args) {
break; break;
} }
default: default:
net.Server.prototype[SymbolFor('nodejs.rejection')] ReflectApply(
.apply(this, arguments); net.Server.prototype[SymbolFor('nodejs.rejection')],
this, arguments);
} }
}); });

View File

@ -38,6 +38,10 @@ const noRestrictedSyntax = [
selector: "ThrowStatement > NewExpression[callee.name=/^ERR_[A-Z_]+$/] > ObjectExpression:first-child:not(:has([key.name='message']):has([key.name='code']):has([key.name='syscall']))", selector: "ThrowStatement > NewExpression[callee.name=/^ERR_[A-Z_]+$/] > ObjectExpression:first-child:not(:has([key.name='message']):has([key.name='code']):has([key.name='syscall']))",
message: 'The context passed into the SystemError constructor must include .code, .syscall, and .message properties.', message: 'The context passed into the SystemError constructor must include .code, .syscall, and .message properties.',
}, },
{
selector: "CallExpression:matches([callee.type='Identifier'][callee.name='FunctionPrototypeApply'], [callee.type='MemberExpression'][callee.property.type='Identifier'][callee.property.name='apply'][arguments.length=2])",
message: 'Use `ReflectApply` instead of %Function.prototype.apply%',
},
]; ];
export default [ export default [
@ -57,7 +61,13 @@ export default [
rules: { rules: {
'prefer-object-spread': 'error', 'prefer-object-spread': 'error',
'no-buffer-constructor': 'error', 'no-buffer-constructor': 'error',
'no-restricted-syntax': noRestrictedSyntax, 'no-restricted-syntax': [
...noRestrictedSyntax,
{
selector: "CallExpression[callee.type='Identifier'][callee.name='ReflectApply'][arguments.2.type='ArrayExpression']",
message: 'Use `FunctionPrototypeCall` to avoid creating an ad-hoc array',
},
],
'no-restricted-globals': [ 'no-restricted-globals': [
'error', 'error',
{ {

View File

@ -505,7 +505,7 @@ EventEmitter.prototype.emit = function emit(type, ...args) {
return false; return false;
if (typeof handler === 'function') { if (typeof handler === 'function') {
const result = handler.apply(this, args); const result = ReflectApply(handler, this, args);
// We check if result is undefined first because that // We check if result is undefined first because that
// is the most common case so we do not pay any perf // is the most common case so we do not pay any perf
@ -517,7 +517,7 @@ EventEmitter.prototype.emit = function emit(type, ...args) {
const len = handler.length; const len = handler.length;
const listeners = arrayClone(handler); const listeners = arrayClone(handler);
for (let i = 0; i < len; ++i) { for (let i = 0; i < len; ++i) {
const result = listeners[i].apply(this, args); const result = ReflectApply(listeners[i], this, args);
// We check if result is undefined first because that // We check if result is undefined first because that
// is the most common case so we do not pay any perf // is the most common case so we do not pay any perf
@ -620,7 +620,7 @@ function onceWrapper() {
this.fired = true; this.fired = true;
if (arguments.length === 0) if (arguments.length === 0)
return this.listener.call(this.target); return this.listener.call(this.target);
return this.listener.apply(this.target, arguments); return ReflectApply(this.listener, this.target, arguments);
} }
} }

View File

@ -5,6 +5,7 @@ const {
ErrorCaptureStackTrace, ErrorCaptureStackTrace,
ObjectDefineProperty, ObjectDefineProperty,
ObjectPrototypeHasOwnProperty, ObjectPrototypeHasOwnProperty,
ReflectApply,
Symbol, Symbol,
} = primordials; } = primordials;
@ -125,9 +126,9 @@ function callbackTrampoline(asyncId, resource, cb, ...args) {
let result; let result;
if (asyncId === 0 && typeof domain_cb === 'function') { if (asyncId === 0 && typeof domain_cb === 'function') {
args.unshift(cb); args.unshift(cb);
result = domain_cb.apply(this, args); result = ReflectApply(domain_cb, this, args);
} else { } else {
result = cb.apply(this, args); result = ReflectApply(cb, this, args);
} }
if (asyncId !== 0 && hasHooks(kAfter)) if (asyncId !== 0 && hasHooks(kAfter))
@ -462,14 +463,14 @@ function clearDefaultTriggerAsyncId() {
*/ */
function defaultTriggerAsyncIdScope(triggerAsyncId, block, ...args) { function defaultTriggerAsyncIdScope(triggerAsyncId, block, ...args) {
if (triggerAsyncId === undefined) if (triggerAsyncId === undefined)
return block.apply(null, args); return ReflectApply(block, null, args);
// CHECK(NumberIsSafeInteger(triggerAsyncId)) // CHECK(NumberIsSafeInteger(triggerAsyncId))
// CHECK(triggerAsyncId > 0) // CHECK(triggerAsyncId > 0)
const oldDefaultTriggerAsyncId = async_id_fields[kDefaultTriggerAsyncId]; const oldDefaultTriggerAsyncId = async_id_fields[kDefaultTriggerAsyncId];
async_id_fields[kDefaultTriggerAsyncId] = triggerAsyncId; async_id_fields[kDefaultTriggerAsyncId] = triggerAsyncId;
try { try {
return block.apply(null, args); return ReflectApply(block, null, args);
} finally { } finally {
async_id_fields[kDefaultTriggerAsyncId] = oldDefaultTriggerAsyncId; async_id_fields[kDefaultTriggerAsyncId] = oldDefaultTriggerAsyncId;
} }

View File

@ -6,7 +6,6 @@ const {
JSONParse, JSONParse,
ObjectAssign, ObjectAssign,
ObjectPrototypeHasOwnProperty, ObjectPrototypeHasOwnProperty,
ReflectApply,
SafeArrayIterator, SafeArrayIterator,
SafeMap, SafeMap,
SafeSet, SafeSet,
@ -198,8 +197,8 @@ function loadCJSModule(module, source, url, filename, isMain) {
}); });
setOwnProperty(requireFn, 'main', process.mainModule); setOwnProperty(requireFn, 'main', process.mainModule);
ReflectApply(compiledWrapper, module.exports, FunctionPrototypeCall(compiledWrapper, module.exports,
[module.exports, requireFn, module, filename, __dirname]); module.exports, requireFn, module, filename, __dirname);
setOwnProperty(module, 'loaded', true); setOwnProperty(module, 'loaded', true);
} }

View File

@ -6,6 +6,7 @@
const { const {
Promise, Promise,
PromisePrototypeThen, PromisePrototypeThen,
ReflectApply,
SymbolDispose, SymbolDispose,
} = primordials; } = primordials;
@ -280,7 +281,7 @@ function eos(stream, options, callback) {
const originalCallback = callback; const originalCallback = callback;
callback = once((...args) => { callback = once((...args) => {
disposable[SymbolDispose](); disposable[SymbolDispose]();
originalCallback.apply(stream, args); ReflectApply(originalCallback, stream, args);
}); });
} }
} }
@ -304,13 +305,13 @@ function eosWeb(stream, options, callback) {
const originalCallback = callback; const originalCallback = callback;
callback = once((...args) => { callback = once((...args) => {
disposable[SymbolDispose](); disposable[SymbolDispose]();
originalCallback.apply(stream, args); ReflectApply(originalCallback, stream, args);
}); });
} }
} }
const resolverFn = (...args) => { const resolverFn = (...args) => {
if (!isAborted) { if (!isAborted) {
process.nextTick(() => callback.apply(stream, args)); process.nextTick(() => ReflectApply(callback, stream, args));
} }
}; };
PromisePrototypeThen( PromisePrototypeThen(

View File

@ -30,6 +30,7 @@ const {
ObjectKeys, ObjectKeys,
ObjectSetPrototypeOf, ObjectSetPrototypeOf,
Promise, Promise,
ReflectApply,
SafeSet, SafeSet,
Symbol, Symbol,
SymbolAsyncDispose, SymbolAsyncDispose,
@ -1182,8 +1183,7 @@ Readable.prototype.removeListener = function(ev, fn) {
Readable.prototype.off = Readable.prototype.removeListener; Readable.prototype.off = Readable.prototype.removeListener;
Readable.prototype.removeAllListeners = function(ev) { Readable.prototype.removeAllListeners = function(ev) {
const res = Stream.prototype.removeAllListeners.apply(this, const res = ReflectApply(Stream.prototype.removeAllListeners, this, arguments);
arguments);
if (ev === 'readable' || ev === undefined) { if (ev === 'readable' || ev === undefined) {
// We need to check if there is someone still listening to // We need to check if there is someone still listening to

View File

@ -5,7 +5,6 @@ const {
ArrayPrototypeIncludes, ArrayPrototypeIncludes,
DatePrototypeGetTime, DatePrototypeGetTime,
DatePrototypeToString, DatePrototypeToString,
FunctionPrototypeApply,
FunctionPrototypeBind, FunctionPrototypeBind,
FunctionPrototypeToString, FunctionPrototypeToString,
NumberIsNaN, NumberIsNaN,
@ -14,6 +13,7 @@ const {
ObjectGetOwnPropertyDescriptor, ObjectGetOwnPropertyDescriptor,
ObjectGetOwnPropertyDescriptors, ObjectGetOwnPropertyDescriptors,
PromiseWithResolvers, PromiseWithResolvers,
ReflectApply,
Symbol, Symbol,
SymbolDispose, SymbolDispose,
globalThis, globalThis,
@ -645,7 +645,7 @@ class MockTimers {
let timer = this.#executionQueue.peek(); let timer = this.#executionQueue.peek();
while (timer) { while (timer) {
if (timer.runAt > this.#now) break; if (timer.runAt > this.#now) break;
FunctionPrototypeApply(timer.callback, undefined, timer.args); ReflectApply(timer.callback, undefined, timer.args);
// Check if the timeout was cleared by calling clearTimeout inside its own callback // Check if the timeout was cleared by calling clearTimeout inside its own callback
const afterCallback = this.#executionQueue.peek(); const afterCallback = this.#executionQueue.peek();