lib: add lint rule to enforce use of kEmptyObject

Signed-off-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/63790
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Jake Yuesong Li <jake.yuesong@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
Antoine du Hamel authored and GitHub committed 2026-06-10 10:56:56 +00:00
1 parent 19c46abbef
commit 1778eeb6e4
25 files changed
+138 -45

No files matched your search

+5 -5
View File
@@ -33,7 +33,7 @@ const { triggerUncaughtException } = internalBinding('errors');
const dc_binding = internalBinding('diagnostics_channel');
const { subscribers: subscriberCounts } = dc_binding;
const { WeakReference } = require('internal/util');
const { WeakReference, kEmptyObject } = require('internal/util');
const { isPromise } = require('internal/util/types');
// Can't delete when weakref count reaches 0 as it could increment again.
@@ -390,7 +390,7 @@ class BoundedChannel {
return done;
}
withScope(context = {}) {
withScope(context = kEmptyObject) {
return new BoundedChannelScope(this, context);
}
@@ -514,7 +514,7 @@ class TracingChannel {
return done;
}
traceSync(fn, context = {}, thisArg, ...args) {
traceSync(fn, context = { __proto__: null }, thisArg, ...args) {
if (!this.hasSubscribers) {
return ReflectApply(fn, thisArg, args);
}
@@ -534,7 +534,7 @@ class TracingChannel {
}
}
tracePromise(fn, context = {}, thisArg, ...args) {
tracePromise(fn, context = { __proto__: null }, thisArg, ...args) {
if (!this.hasSubscribers) {
const result = ReflectApply(fn, thisArg, args);
if (typeof result?.then !== 'function') {
@@ -589,7 +589,7 @@ class TracingChannel {
}
}
traceCallback(fn, position = -1, context = {}, thisArg, ...args) {
traceCallback(fn, position = -1, context = kEmptyObject, thisArg, ...args) {
if (!this.hasSubscribers) {
return ReflectApply(fn, thisArg, args);
}
+8 -8
View File
@@ -1840,7 +1840,7 @@ function readdirSync(path, options) {
* ) => any} [callback]
* @returns {void}
*/
function fstat(fd, options = { bigint: false }, callback) {
function fstat(fd, options = { __proto__: null, bigint: false }, callback) {
if (typeof options === 'function') {
callback = options;
options = kEmptyObject;
@@ -1867,7 +1867,7 @@ function fstat(fd, options = { bigint: false }, callback) {
* ) => any} callback
* @returns {void}
*/
function lstat(path, options = { bigint: false }, callback) {
function lstat(path, options = { __proto__: null, bigint: false }, callback) {
if (typeof options === 'function') {
callback = options;
options = kEmptyObject;
@@ -1899,7 +1899,7 @@ function lstat(path, options = { bigint: false }, callback) {
* ) => any} callback
* @returns {void}
*/
function stat(path, options = { bigint: false, throwIfNoEntry: true }, callback) {
function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }, callback) {
if (typeof options === 'function') {
callback = options;
options = kEmptyObject;
@@ -1922,7 +1922,7 @@ function stat(path, options = { bigint: false, throwIfNoEntry: true }, callback)
binding.stat(getValidatedPath(path), options.bigint, req, options.throwIfNoEntry);
}
function statfs(path, options = { bigint: false }, callback) {
function statfs(path, options = { __proto__: null, bigint: false }, callback) {
if (typeof options === 'function') {
callback = options;
options = kEmptyObject;
@@ -1957,7 +1957,7 @@ function statfs(path, options = { bigint: false }, callback) {
* @param {{ bigint?: boolean; }} [options]
* @returns {Stats | undefined}
*/
function fstatSync(fd, options = { bigint: false }) {
function fstatSync(fd, options = { __proto__: null, bigint: false }) {
const h = vfsState.handlers;
if (h !== null) {
const result = h.fstatSync(fd);
@@ -1980,7 +1980,7 @@ function fstatSync(fd, options = { bigint: false }) {
* }} [options]
* @returns {Stats | undefined}
*/
function lstatSync(path, options = { bigint: false, throwIfNoEntry: true }) {
function lstatSync(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) {
const h = vfsState.handlers;
if (h !== null) {
const result = h.lstatSync(path, options);
@@ -2014,7 +2014,7 @@ function lstatSync(path, options = { bigint: false, throwIfNoEntry: true }) {
* }} [options]
* @returns {Stats}
*/
function statSync(path, options = { bigint: false, throwIfNoEntry: true }) {
function statSync(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) {
const h = vfsState.handlers;
if (h !== null) {
const result = h.statSync(path, options);
@@ -2032,7 +2032,7 @@ function statSync(path, options = { bigint: false, throwIfNoEntry: true }) {
return getStatsFromBinding(stats);
}
function statfsSync(path, options = { bigint: false }) {
function statfsSync(path, options = { __proto__: null, bigint: false }) {
const h = vfsState.handlers;
if (h !== null) {
const result = h.statfsSync(path, options);
@@ -13,6 +13,7 @@ const AsyncContextFrame = require('internal/async_context_frame');
const { AsyncResource } = require('async_hooks');
const RunScope = require('internal/async_local_storage/run_scope');
const { kEmptyObject } = require('internal/util');
class AsyncLocalStorage {
#defaultValue = undefined;
@@ -26,7 +27,7 @@ class AsyncLocalStorage {
/**
* @param {AsyncLocalStorageOptions} [options]
*/
constructor(options = {}) {
constructor(options = kEmptyObject) {
validateObject(options, 'options');
this.#defaultValue = options.defaultValue;
@@ -20,6 +20,7 @@ const {
} = require('async_hooks');
const RunScope = require('internal/async_local_storage/run_scope');
const { kEmptyObject } = require('internal/util');
const storageList = [];
const storageHook = createHook({
@@ -44,7 +45,7 @@ class AsyncLocalStorage {
/**
* @param {AsyncLocalStorageOptions} [options]
*/
constructor(options = {}) {
constructor(options = kEmptyObject) {
this.kResourceStore = Symbol('kResourceStore');
this.enabled = false;
validateObject(options, 'options');
+1 -1
View File
@@ -541,7 +541,7 @@ function createBlobReaderStream(reader) {
// unbounded memory growth when the DataQueue has a large burst of data.
const kMaxBatchChunks = 16;
async function* createBlobReaderIterable(reader, options = {}) {
async function* createBlobReaderIterable(reader, options = kEmptyObject) {
const { getReadError } = options;
let wakeup = PromiseWithResolvers();
reader.setWakeup(wakeup.resolve);
+2 -2
View File
@@ -58,7 +58,7 @@ const assert = require('internal/assert');
const LazyTransform = require('internal/streams/lazy_transform');
const { normalizeEncoding } = require('internal/util');
const { normalizeEncoding, kEmptyObject } = require('internal/util');
const { StringDecoder } = require('string_decoder');
@@ -255,7 +255,7 @@ addCipherPrototypeFunctions(Decipheriv);
const kMinNid = 1;
const kMaxNid = 2_147_483_647;
function getCipherInfo(nameOrNid, options = {}) {
function getCipherInfo(nameOrNid, options = kEmptyObject) {
validateObject(options, 'options');
let { keyLength, ivLength } = options;
if (keyLength !== undefined) {
+4 -4
View File
@@ -1715,7 +1715,7 @@ async function symlink(target, path, type) {
);
}
async function fstat(handle, options = { bigint: false }) {
async function fstat(handle, options = { __proto__: null, bigint: false }) {
const result = await PromisePrototypeThen(
binding.fstat(handle.fd, options.bigint, kUsePromises),
undefined,
@@ -1724,7 +1724,7 @@ async function fstat(handle, options = { bigint: false }) {
return getStatsFromBinding(result);
}
async function lstat(path, options = { bigint: false }) {
async function lstat(path, options = { __proto__: null, bigint: false }) {
const h = vfsState.handlers;
if (h !== null) {
const promise = h.lstat(path, options);
@@ -1743,7 +1743,7 @@ async function lstat(path, options = { bigint: false }) {
return getStatsFromBinding(result);
}
async function stat(path, options = { bigint: false, throwIfNoEntry: true }) {
async function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) {
const h = vfsState.handlers;
if (h !== null) {
const promise = h.stat(path, options);
@@ -1761,7 +1761,7 @@ async function stat(path, options = { bigint: false, throwIfNoEntry: true }) {
return getStatsFromBinding(result);
}
async function statfs(path, options = { bigint: false }) {
async function statfs(path, options = { __proto__: null, bigint: false }) {
const h = vfsState.handlers;
if (h !== null) {
const result = h.statfs(path, options);
+1 -1
View File
@@ -3813,7 +3813,7 @@ function getUnpackedSettings(buf, options = kEmptyObject) {
return settings;
}
function performServerHandshake(socket, options = {}) {
function performServerHandshake(socket, options = kEmptyObject) {
options = initializeOptions(options);
return new ServerHttp2Session(options, socket, undefined);
}
+2 -1
View File
@@ -19,6 +19,7 @@ const {
} = require('internal/inspector/network');
const { Network } = require('inspector');
const EventEmitter = require('events');
const { kEmptyObject } = require('internal/util');
const kRequestUrl = Symbol('kRequestUrl');
@@ -36,7 +37,7 @@ function getRequestURL(request, host) {
}
// Convert a Headers object (Map<string, number | string | string[]>) to a plain object (Map<string, string>)
const convertHeaderObject = (headers = {}) => {
const convertHeaderObject = (headers = kEmptyObject) => {
// The 'host' header that contains the host and port of the URL.
let host;
let charset;
+2 -1
View File
@@ -30,11 +30,12 @@ const {
} = internalBinding('http2').constants;
const EventEmitter = require('events');
const { Buffer } = require('buffer');
const { kEmptyObject } = require('internal/util');
const kRequestUrl = Symbol('kRequestUrl');
// Convert a Headers object (Map<string, number | string | string[]>) to a plain object (Map<string, string>)
function convertHeaderObject(headers = {}) {
function convertHeaderObject(headers = kEmptyObject) {
let scheme;
let authority;
let path;
+1 -1
View File
@@ -354,7 +354,7 @@ class AsyncLoaderHooksOnLoaderHookWorker {
* @param {object} context Metadata about the module
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
*/
async load(url, context = {}) {
async load(url, context = kEmptyObject) {
const chain = this.#chains.load;
const meta = {
chainFinished: null,
+2 -2
View File
@@ -30,7 +30,7 @@ const { getOptionValue } = require('internal/options');
// Do not eagerly grab .manifest, it may be in TDZ
const { sep, posix: { relative: relativePosixPath }, resolve } = require('path');
const { URL, pathToFileURL, fileURLToPath, isURL, URLParse } = require('internal/url');
const { getCWDURL, setOwnProperty } = require('internal/util');
const { getCWDURL, setOwnProperty, kEmptyObject } = require('internal/util');
const { canParse: URLCanParse } = internalBinding('url');
const { legacyMainResolve: FSLegacyMainResolve } = internalBinding('fs');
const {
@@ -944,7 +944,7 @@ function throwIfInvalidParentURL(parentURL) {
* @param {string[]} [context.conditions] - The conditions for resolving the specifier.
* @returns {{url: string, format?: string}}
*/
function defaultResolve(specifier, context = {}) {
function defaultResolve(specifier, context = kEmptyObject) {
let { parentURL, conditions } = context;
throwIfInvalidParentURL(parentURL);
+2 -2
View File
@@ -543,14 +543,14 @@ function hasObserver(type) {
}
function startPerf(target, key, context = {}) {
function startPerf(target, key, context = kEmptyObject) {
target[key] = {
...context,
startTime: now(),
};
}
function stopPerf(target, key, context = {}) {
function stopPerf(target, key, context = kEmptyObject) {
const ctx = target[key];
if (!ctx) {
return;
+1 -1
View File
@@ -31,7 +31,7 @@ const ESCAPE_CODE_TIMEOUT = 500;
* accepts a readable Stream instance and makes it emit "keypress" events
*/
function emitKeypressEvents(stream, iface = {}) {
function emitKeypressEvents(stream, iface = { __proto__: null }) {
if (stream[KEYPRESS_DECODER]) return;
stream[KEYPRESS_DECODER] = new StringDecoder('utf8');
+2 -1
View File
@@ -12,6 +12,7 @@ const {
const {
sleep,
kEmptyObject,
} = require('internal/util');
const {
@@ -131,7 +132,7 @@ class Utf8Stream extends EventEmitter {
contentMode = kContentModeUtf8,
mode,
// Provides for a custom fs implementation. Mostly useful for testing.
fs: overrideFs = {},
fs: overrideFs = kEmptyObject,
} = options;
super();
+2 -1
View File
@@ -4,6 +4,7 @@ const {
MathFloor,
} = primordials;
const { kEmptyObject } = require('internal/util');
const {
validateNumber,
validateObject,
@@ -14,7 +15,7 @@ const kMaxSamplingIntervalUs = 0x7FFFFFFF;
const kMaxSamplingIntervalMs = kMaxSamplingIntervalUs / kMicrosPerMilli;
const kMaxSamplesUnlimited = 0xFFFF_FFFF;
function normalizeCpuProfileOptions(options = {}) {
function normalizeCpuProfileOptions(options = kEmptyObject) {
validateObject(options, 'options');
// TODO(ishabi): add support for 'mode' and 'filterContext' options
+2 -1
View File
@@ -1,5 +1,6 @@
'use strict';
const { kEmptyObject } = require('internal/util');
const {
validateBoolean,
validateInteger,
@@ -14,7 +15,7 @@ const {
kSamplingIncludeObjectsCollectedByMinorGC,
} = internalBinding('v8');
function normalizeHeapProfileOptions(options = {}) {
function normalizeHeapProfileOptions(options = kEmptyObject) {
validateObject(options, 'options');
const {
sampleInterval = 512 * 1024,
+4 -3
View File
@@ -18,6 +18,7 @@ const {
} = internalBinding('constants');
const { getStatsFromBinding } = require('internal/fs/utils');
const { kEmptyObject } = require('internal/util');
// Default block size for virtual files (4KB)
const kDefaultBlockSize = 4096;
@@ -91,7 +92,7 @@ function fillBigIntStatsArray(
* @param {boolean} [options.bigint] Return BigIntStats
* @returns {Stats}
*/
function createFileStats(size, options = {}) {
function createFileStats(size, options = kEmptyObject) {
const now = DateNow();
const mode = (options.mode ?? 0o644) | S_IFREG;
const nlink = options.nlink ?? 1;
@@ -150,7 +151,7 @@ function createFileStats(size, options = {}) {
* @param {number} [options.birthtimeMs] Birth time in ms
* @returns {Stats}
*/
function createDirectoryStats(options = {}) {
function createDirectoryStats(options = kEmptyObject) {
const now = DateNow();
const mode = (options.mode ?? 0o755) | S_IFDIR;
const uid = options.uid ?? (process.getuid?.() ?? 0);
@@ -208,7 +209,7 @@ function createDirectoryStats(options = {}) {
* @param {number} [options.birthtimeMs] Birth time in ms
* @returns {Stats}
*/
function createSymlinkStats(size, options = {}) {
function createSymlinkStats(size, options = kEmptyObject) {
const now = DateNow();
const mode = (options.mode ?? 0o777) | S_IFLNK;
const uid = options.uid ?? (process.getuid?.() ?? 0);
+4 -3
View File
@@ -18,6 +18,7 @@ const {
setInterval,
clearInterval,
} = require('timers');
const { kEmptyObject } = require('internal/util');
/**
* VFSWatcher - Polling-based file/directory watcher for VFS.
@@ -47,7 +48,7 @@ class VFSWatcher extends EventEmitter {
* @param {boolean} [options.recursive] Watch subdirectories (default: false)
* @param {AbortSignal} [options.signal] AbortSignal for cancellation
*/
constructor(provider, path, options = {}) {
constructor(provider, path, options = kEmptyObject) {
super();
this.#vfs = provider;
@@ -395,7 +396,7 @@ class VFSStatWatcher extends EventEmitter {
* @param {number} [options.interval] Polling interval in ms (default: 5007)
* @param {boolean} [options.persistent] Keep process alive (default: true)
*/
constructor(provider, path, options = {}) {
constructor(provider, path, options = kEmptyObject) {
super();
this.#vfs = provider;
@@ -586,7 +587,7 @@ class VFSWatchAsyncIterable {
* @param {string} path The path to watch (provider-relative)
* @param {object} [options] Options
*/
constructor(provider, path, options = {}) {
constructor(provider, path, options = kEmptyObject) {
// Strip signal from options passed to VFSWatcher - we handle abort
// at the iterable level to reject pending next() with AbortError
// instead of resolving with done:true via the 'close' event.
+1 -1
View File
@@ -967,7 +967,7 @@ class REPLServer extends Interface {
self.displayPrompt();
}
setupHistory(historyConfig = {}, cb) {
setupHistory(historyConfig = { __proto__: null }, cb) {
// TODO(puskin94): necessary because historyConfig can be a string for backwards compatibility
const options = typeof historyConfig === 'string' ?
{ filePath: historyConfig } :
+2 -1
View File
@@ -96,6 +96,7 @@ const {
getSystemErrorMessage: internalErrorMessage,
promisify,
defineLazyProperties,
kEmptyObject,
} = require('internal/util');
let abortController;
@@ -584,7 +585,7 @@ function getCallSites(frameCount = 10, options) {
};
// Public util.deprecate API
function deprecate(fn, msg, code, { modifyPrototype } = {}) {
function deprecate(fn, msg, code, { modifyPrototype } = kEmptyObject) {
return internalDeprecate(fn, msg, code, undefined, modifyPrototype);
}
+2 -1
View File
@@ -24,6 +24,7 @@
const {
ArrayPrototypeForEach,
ObjectFreeze,
ObjectPrototype,
PromiseReject,
ReflectApply,
Symbol,
@@ -222,7 +223,7 @@ function getContextOptions(options) {
}
let defaultContextNameIndex = 1;
function createContext(contextObject = {}, options = kEmptyObject) {
function createContext(contextObject = { __proto__: ObjectPrototype }, options = kEmptyObject) {
if (contextObject !== vm_context_no_contextify && isContext(contextObject)) {
return contextObject;
}
+1 -1
View File
@@ -111,7 +111,7 @@ class WASI {
finalizeBindings(instance, {
memory = instance?.exports?.memory,
} = {}) {
} = kEmptyObject) {
if (this[kStarted]) {
throw new ERR_WASI_ALREADY_STARTED();
}
@@ -11,7 +11,7 @@ const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester;
const rule = require('../../tools/eslint-rules/avoid-prototype-pollution');
new RuleTester()
.run('property-descriptor-no-prototype-pollution', rule, {
.run('avoid-prototype-pollution', rule, {
valid: [
'ObjectDefineProperties({}, {})',
'ObjectCreate(null, {})',
@@ -63,6 +63,18 @@ new RuleTester()
'new Proxy({}, { __proto__: null, ...{} })',
'async function name(){return await SafePromiseAll([])}',
'async function name(){const val = await SafePromiseAll([])}',
'async function name(options = kEmptyObject){}',
'function name(options = kEmptyObject){}',
'function name(options = { __proto__: ObjectPrototype }){}',
'function name(options = { __proto__: null }){}',
'new class { name(options = kEmptyObject){} }',
'const name = (options = kEmptyObject) => {}',
'async function name(options = { __proto__: null, key: 1 }){}',
'const name = ({ destr } = { destr: 1 }) => {}',
'function name({ destr } = kEmptyObject){}',
'function name({ [Symbol.match]: m } = kEmptyObject){}',
'function name({ [Symbol.match]: m } = { __proto__: null, [Symbol.match]: 1 }){}',
'function name({ [Symbol.match]: m, c } = { __proto__: null, c: 1 }){}',
],
invalid: [
{
@@ -330,5 +342,53 @@ new RuleTester()
code: 'ArrayPrototypeConcat([])',
errors: [{ message: /\bisConcatSpreadable\b/ }]
},
{
code: 'function name(options = {}) {}',
errors: [{ message: /\bkEmptyObject\b/ }]
},
{
code: 'async function name(options = { key: 1 }) {}',
errors: [{ message: /\b__proto__: null\b/ }]
},
{
code: 'new class { name(options = {}) {} }',
errors: [{ message: /\bkEmptyObject\b/ }]
},
{
code: 'new class { async name(options = {}) {} }',
errors: [{ message: /\bkEmptyObject\b/ }]
},
{
code: 'const name = (options = {}) => {}',
errors: [{ message: /\bkEmptyObject\b/ }]
},
{
code: 'const name = async (options = {}) => {}',
errors: [{ message: /\bkEmptyObject\b/ }]
},
{
code: 'function name({ destr } = {}) {}',
errors: [{ message: /\bkEmptyObject\b/ }]
},
{
code: 'async function name({ destr } = {}) {}',
errors: [{ message: /\bkEmptyObject\b/ }]
},
{
code: 'new class { name({ destr } = {}) {} }',
errors: [{ message: /\bkEmptyObject\b/ }]
},
{
code: 'new class { async name({ destr } = {}) {} }',
errors: [{ message: /\bkEmptyObject\b/ }]
},
{
code: 'const name = ({ destr } = {}) => {}',
errors: [{ message: /\bkEmptyObject\b/ }]
},
{
code: 'const name = async ({ destr } = {}) => {}',
errors: [{ message: /\bkEmptyObject\b/ }]
},
]
});
@@ -232,6 +232,28 @@ module.exports = {
'which can be subject to prototype pollution',
});
},
'AssignmentPattern[right.type="ObjectExpression"]'(node) {
if (!node.right.properties.length) {
context.report({
node: node.right,
message: 'Use kEmptyObject instead of declaring a new empty object, or define a __proto__ property',
});
return;
}
const propertyIsIdentifier = (p) => p.key.type === 'Identifier';
if (node.left.type === 'ObjectPattern' &&
node.left.properties.every(propertyIsIdentifier) &&
node.right.properties.every(propertyIsIdentifier)) {
const rightNames = node.right.properties.map((p) => p.key.name);
if (node.left.properties.every((p) => rightNames.includes(p.key.name))) return;
}
if (node.right.properties.some((p) => p.key.name === '__proto__')) return;
context.report({
node: node.right,
message: `Add '__proto__: null' to avoid inheriting from Object.prototype, or '__proto__: ObjectPrototype' if inheritance is desirable`,
});
},
};
},
};