From 574f2dd517a0dbd2e85f8db067cc15822c772e85 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Tue, 24 Sep 2024 15:48:15 -0400 Subject: [PATCH] lib: prefer optional chaining PR-URL: https://github.com/nodejs/node/pull/55045 Reviewed-By: James M Snell Reviewed-By: Matteo Collina Reviewed-By: Yagiz Nizipli Reviewed-By: Chemi Atlow Reviewed-By: Trivikram Kamat Reviewed-By: Paolo Insogna Reviewed-By: Marco Ippolito Reviewed-By: Stephen Belanger --- eslint.config.mjs | 1 + lib/_http_agent.js | 6 +- lib/_http_client.js | 10 +- lib/_http_common.js | 3 +- lib/_http_incoming.js | 2 +- lib/_http_outgoing.js | 8 +- lib/_http_server.js | 10 +- lib/_tls_wrap.js | 4 +- lib/assert.js | 6 +- lib/child_process.js | 8 +- lib/dgram.js | 4 +- .../bootstrap/switches/is_main_thread.js | 2 +- lib/internal/child_process.js | 8 +- lib/internal/cluster/child.js | 2 +- lib/internal/crypto/hashnames.js | 2 +- lib/internal/debugger/inspect_repl.js | 4 +- lib/internal/dns/callback_resolver.js | 2 +- lib/internal/dns/promises.js | 2 +- lib/internal/error_serdes.js | 4 +- lib/internal/modules/esm/loader.js | 2 +- lib/internal/modules/esm/translators.js | 4 +- lib/internal/options.js | 18 +--- lib/internal/readline/interface.js | 4 +- lib/internal/source_map/source_map.js | 2 +- lib/internal/streams/from.js | 4 +- lib/internal/streams/pipeline.js | 2 +- lib/internal/streams/readable.js | 10 +- lib/internal/streams/utils.js | 3 +- lib/internal/streams/writable.js | 8 +- lib/internal/util/inspect.js | 2 +- lib/net.js | 7 +- lib/readline.js | 2 +- lib/repl.js | 6 +- lib/timers.js | 2 +- lib/zlib.js | 2 +- test/async-hooks/init-hooks.js | 8 +- test/common/dns.js | 10 +- test/parallel/test-cluster-setup-primary.js | 3 +- test/parallel/test-cluster-worker-exit.js | 2 +- test/parallel/test-cluster-worker-kill.js | 2 +- ...n-throw-from-uncaught-exception-handler.js | 2 +- .../test-eslint-prefer-optional-chaining.js | 34 +++++++ test/parallel/test-module-relative-lookup.js | 2 +- test/parallel/test-readline-interface.js | 2 +- .../test-readline-promises-interface.js | 2 +- .../parallel/test-stream2-large-read-stall.js | 4 +- .../test-tls-check-server-identity.js | 4 +- .../test-trace-events-dynamic-enable.js | 2 +- tools/doc/markdown.mjs | 2 +- tools/eslint-rules/no-unescaped-regexp-dot.js | 5 +- .../eslint-rules/prefer-optional-chaining.js | 93 +++++++++++++++++++ tools/eslint-rules/rules-utils.js | 6 +- 52 files changed, 228 insertions(+), 121 deletions(-) create mode 100644 test/parallel/test-eslint-prefer-optional-chaining.js create mode 100644 tools/eslint-rules/prefer-optional-chaining.js diff --git a/eslint.config.mjs b/eslint.config.mjs index 67d21b540f9..2ba63203d8e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -313,6 +313,7 @@ export default [ 'node-core/no-unescaped-regexp-dot': 'error', 'node-core/no-duplicate-requires': 'error', 'node-core/prefer-proto': 'error', + 'node-core/prefer-optional-chaining': 'error', }, }, // #endregion diff --git a/lib/_http_agent.js b/lib/_http_agent.js index 1f7989b843c..7e5c224470c 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -128,7 +128,7 @@ function Agent(options) { } const requests = this.requests[name]; - if (requests && requests.length) { + if (requests?.length) { const req = requests.shift(); const reqAsyncRes = req[kRequestAsyncResource]; if (reqAsyncRes) { @@ -437,7 +437,7 @@ Agent.prototype.removeSocket = function removeSocket(s, options) { } let req; - if (this.requests[name] && this.requests[name].length) { + if (this.requests[name]?.length) { debug('removeSocket, have a request, make a socket'); req = this.requests[name][0]; } else { @@ -449,7 +449,7 @@ Agent.prototype.removeSocket = function removeSocket(s, options) { for (let i = 0; i < keys.length; i++) { const prop = keys[i]; // Check whether this specific origin is already at maxSockets - if (this.sockets[prop] && this.sockets[prop].length) break; + if (this.sockets[prop]?.length) break; debug('removeSocket, have a request with different origin,' + ' make a socket'); req = this.requests[prop][0]; diff --git a/lib/_http_client.js b/lib/_http_client.js index 6d3b3591e7b..750536dd06c 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -174,7 +174,7 @@ function ClientRequest(input, options, cb) { const protocol = options.protocol || defaultAgent.protocol; let expectedProtocol = defaultAgent.protocol; - if (this.agent && this.agent.protocol) + if (this.agent?.protocol) expectedProtocol = this.agent.protocol; if (options.path) { @@ -190,7 +190,7 @@ function ClientRequest(input, options, cb) { } const defaultPort = options.defaultPort || - (this.agent && this.agent.defaultPort); + (this.agent?.defaultPort); const optsWithoutSignal = { __proto__: null, ...options }; @@ -553,7 +553,7 @@ function socketOnData(d) { socket.destroy(); req.socket._hadError = true; emitErrorEvent(req, ret); - } else if (parser.incoming && parser.incoming.upgrade) { + } else if (parser.incoming?.upgrade) { // Upgrade (if status code 101) or CONNECT const bytesParsed = ret; const res = parser.incoming; @@ -591,7 +591,7 @@ function socketOnData(d) { // Requested Upgrade or used CONNECT method, but have no handler. socket.destroy(); } - } else if (parser.incoming && parser.incoming.complete && + } else if (parser.incoming?.complete && // When the status code is informational (100, 102-199), // the server will send a final response after this client // sends a request body, so we must not free the parser. @@ -838,7 +838,7 @@ function tickOnSocket(req, socket) { if ( req.timeout !== undefined || - (req.agent && req.agent.options && req.agent.options.timeout) + (req.agent?.options?.timeout) ) { listenSocketTimeout(req); } diff --git a/lib/_http_common.js b/lib/_http_common.js index 5ccf43e1d66..96d9bdfc9fc 100644 --- a/lib/_http_common.js +++ b/lib/_http_common.js @@ -85,8 +85,7 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method, } // Parser is also used by http client - const ParserIncomingMessage = (socket && socket.server && - socket.server[kIncomingMessage]) || + const ParserIncomingMessage = (socket?.server?.[kIncomingMessage]) || IncomingMessage; const incoming = parser.incoming = new ParserIncomingMessage(socket); diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js index 1dd04fdf3e4..c3e901e53e8 100644 --- a/lib/_http_incoming.js +++ b/lib/_http_incoming.js @@ -242,7 +242,7 @@ IncomingMessage.prototype._destroy = function _destroy(err, cb) { IncomingMessage.prototype._addHeaderLines = _addHeaderLines; function _addHeaderLines(headers, n) { - if (headers && headers.length) { + if (headers?.length) { let dest; if (this.complete) { this.rawTrailers = headers; diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 658ad80f994..7a402a0f900 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -423,7 +423,7 @@ OutgoingMessage.prototype._send = function _send(data, encoding, callback, byteL OutgoingMessage.prototype._writeRaw = _writeRaw; function _writeRaw(data, encoding, callback, size) { const conn = this[kSocket]; - if (conn && conn.destroyed) { + if (conn?.destroyed) { // The socket was destroyed. If we're still trying to write to it, // then we haven't gotten the 'close' event yet. return false; @@ -789,7 +789,7 @@ OutgoingMessage.prototype.getHeader = function getHeader(name) { return; const entry = headers[name.toLowerCase()]; - return entry && entry[1]; + return entry?.[1]; }; @@ -1073,7 +1073,7 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { }; function onFinish(outmsg) { - if (outmsg && outmsg.socket && outmsg.socket._hadError) return; + if (outmsg?.socket?._hadError) return; outmsg.emit('finish'); } @@ -1188,7 +1188,7 @@ OutgoingMessage.prototype._finish = function _finish() { OutgoingMessage.prototype._flush = function _flush() { const socket = this[kSocket]; - if (socket && socket.writable) { + if (socket?.writable) { // There might be remaining data in this.output; write it out const ret = this._flushOutput(socket); diff --git a/lib/_http_server.js b/lib/_http_server.js index 200e7a731da..6a0bcd75513 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -738,7 +738,7 @@ function connectionListenerInternal(server, socket) { socket.setEncoding = socketSetEncoding; // We only consume the socket if it has never been consumed before. - if (socket._handle && socket._handle.isStreamBase && + if (socket._handle?.isStreamBase && !socket._handle._consumed) { parser._consumed = true; socket._handle._consumed = true; @@ -783,7 +783,7 @@ function socketOnDrain(socket, state) { } function socketOnTimeout() { - const req = this.parser && this.parser.incoming; + const req = this.parser?.incoming; const reqTimeout = req && !req.complete && req.emit('timeout', this); const res = this._httpMessage; const resTimeout = res && res.emit('timeout', this); @@ -918,7 +918,7 @@ function onParserExecuteCommon(server, socket, parser, state, ret, d) { prepareError(ret, parser, d); debug('parse error', ret); socketOnError.call(socket, ret); - } else if (parser.incoming && parser.incoming.upgrade) { + } else if (parser.incoming?.upgrade) { // Upgrade or CONNECT const req = parser.incoming; debug('SERVER upgrade or connect', req.method); @@ -963,7 +963,7 @@ function onParserExecuteCommon(server, socket, parser, state, ret, d) { function clearIncoming(req) { req = req || this; - const parser = req.socket && req.socket.parser; + const parser = req.socket?.parser; // Reset the .incoming property so that the request object can be gc'ed. if (parser && parser.incoming === req) { if (req.readableEnded) { @@ -1180,7 +1180,7 @@ function onSocketResume() { } function onSocketPause() { - if (this._handle && this._handle.reading) { + if (this._handle?.reading) { this._handle.reading = false; this._handle.readStop(); } diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index 2273b3c91b6..6818515d6ab 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -489,7 +489,7 @@ function initRead(tlsSocket, socket) { return; // Socket already has some buffered data - emulate receiving it - if (socket && socket.readableLength) { + if (socket?.readableLength) { let buf; while ((buf = socket.read()) !== null) tlsSocket._handle.receive(buf); @@ -1683,7 +1683,7 @@ function onConnectSecure() { if (!verifyError && !this.isSessionReused()) { const hostname = options.servername || options.host || - (options.socket && options.socket._host) || + (options.socket?._host) || 'localhost'; const cert = this.getPeerCertificate(true); verifyError = options.checkServerIdentity(hostname, cert); diff --git a/lib/assert.js b/lib/assert.js index 9e54983c868..d121d2718df 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -446,7 +446,7 @@ function expectedException(actual, expected, message, fn) { message = 'The error is expected to be an instance of ' + `"${expected.name}". Received `; if (isError(actual)) { - const name = (actual.constructor && actual.constructor.name) || + const name = (actual.constructor?.name) || actual.name; if (expected.name === name) { message += 'an error with identical name but a different prototype.'; @@ -569,7 +569,7 @@ function expectsError(stackStartFn, actual, error, message) { if (actual === NO_EXCEPTION_SENTINEL) { let details = ''; - if (error && error.name) { + if (error?.name) { details += ` (${error.name})`; } details += message ? `: ${message}` : '.'; @@ -627,7 +627,7 @@ function expectsNoError(stackStartFn, actual, error, message) { expected: error, operator: stackStartFn.name, message: `Got unwanted ${fnType}${details}\n` + - `Actual message: "${actual && actual.message}"`, + `Actual message: "${actual?.message}"`, stackStartFn, }); } diff --git a/lib/child_process.js b/lib/child_process.js index 580a441a803..51fc6fe995d 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -392,8 +392,7 @@ function execFile(file, args, options, callback) { let stderr; if (encoding || ( - child.stdout && - child.stdout.readableEncoding + child.stdout?.readableEncoding )) { stdout = ArrayPrototypeJoin(_stdout, ''); } else { @@ -401,8 +400,7 @@ function execFile(file, args, options, callback) { } if (encoding || ( - child.stderr && - child.stderr.readableEncoding + child.stderr?.readableEncoding )) { stderr = ArrayPrototypeJoin(_stderr, ''); } else { @@ -855,7 +853,7 @@ function spawnSync(file, args, options) { // We may want to pass data in on any given fd, ensure it is a valid buffer for (let i = 0; i < options.stdio.length; i++) { - const input = options.stdio[i] && options.stdio[i].input; + const input = options.stdio[i]?.input; if (input != null) { const pipe = options.stdio[i] = { ...options.stdio[i] }; if (isArrayBufferView(input)) { diff --git a/lib/dgram.js b/lib/dgram.js index 5f93526e301..84b8708f701 100644 --- a/lib/dgram.js +++ b/lib/dgram.js @@ -128,8 +128,8 @@ function Socket(type, listener) { bindState: BIND_STATE_UNBOUND, connectState: CONNECT_STATE_DISCONNECTED, queue: undefined, - reuseAddr: options && options.reuseAddr, // Use UV_UDP_REUSEADDR if true. - ipv6Only: options && options.ipv6Only, + reuseAddr: options?.reuseAddr, // Use UV_UDP_REUSEADDR if true. + ipv6Only: options?.ipv6Only, recvBufferSize, sendBufferSize, }; diff --git a/lib/internal/bootstrap/switches/is_main_thread.js b/lib/internal/bootstrap/switches/is_main_thread.js index 9d709ca743b..74486ba5310 100644 --- a/lib/internal/bootstrap/switches/is_main_thread.js +++ b/lib/internal/bootstrap/switches/is_main_thread.js @@ -251,7 +251,7 @@ function getStdin() { // `stdin` starts out life in a paused state, but node doesn't // know yet. Explicitly to readStop() it to put it in the // not-reading state. - if (stdin._handle && stdin._handle.readStop) { + if (stdin._handle?.readStop) { stdin._handle.reading = false; stdin._readableState.reading = false; stdin._handle.readStop(); diff --git a/lib/internal/child_process.js b/lib/internal/child_process.js index c694a8f26fe..ab6872e4f89 100644 --- a/lib/internal/child_process.js +++ b/lib/internal/child_process.js @@ -854,7 +854,7 @@ function setupChannel(target, channel, serializationMode) { if (handle) { if (!this._handleQueue) this._handleQueue = []; - if (obj && obj.postSend) + if (obj?.postSend) obj.postSend(message, handle, options, callback, target); } @@ -870,7 +870,7 @@ function setupChannel(target, channel, serializationMode) { } } else { // Cleanup handle on error - if (obj && obj.postSend) + if (obj?.postSend) obj.postSend(message, handle, options, callback); if (!options.swallowErrors) { @@ -1115,8 +1115,8 @@ function spawnSync(options) { } } - result.stdout = result.output && result.output[1]; - result.stderr = result.output && result.output[2]; + result.stdout = result.output?.[1]; + result.stderr = result.output?.[2]; if (result.error) { result.error = new ErrnoException(result.error, 'spawnSync ' + options.file); diff --git a/lib/internal/cluster/child.js b/lib/internal/cluster/child.js index 501fd6d739a..bee474567b6 100644 --- a/lib/internal/cluster/child.js +++ b/lib/internal/cluster/child.js @@ -122,7 +122,7 @@ cluster._getServer = function(obj, options, cb) { cluster.worker.state = 'listening'; const address = obj.address(); message.act = 'listening'; - message.port = (address && address.port) || options.port; + message.port = (address?.port) || options.port; send(message); }); }; diff --git a/lib/internal/crypto/hashnames.js b/lib/internal/crypto/hashnames.js index 54cd87b6ba3..49729034fac 100644 --- a/lib/internal/crypto/hashnames.js +++ b/lib/internal/crypto/hashnames.js @@ -70,7 +70,7 @@ function normalizeHashName(name, context = kHashContextNode) { if (typeof name !== 'string') return name; name = StringPrototypeToLowerCase(name); - const alias = kHashNames[name] && kHashNames[name][context]; + const alias = kHashNames[name]?.[context]; return alias || name; } diff --git a/lib/internal/debugger/inspect_repl.js b/lib/internal/debugger/inspect_repl.js index b4f454152dc..8dc5e9823ca 100644 --- a/lib/internal/debugger/inspect_repl.js +++ b/lib/internal/debugger/inspect_repl.js @@ -694,7 +694,7 @@ function createRepl(inspector) { function handleBreakpointResolved({ breakpointId, location }) { const script = knownScripts[location.scriptId]; - const scriptUrl = script && script.url; + const scriptUrl = script?.url; if (scriptUrl) { ObjectAssign(location, { scriptUrl }); } @@ -733,7 +733,7 @@ function createRepl(inspector) { function setBreakpoint(script, line, condition, silent) { function registerBreakpoint({ breakpointId, actualLocation }) { handleBreakpointResolved({ breakpointId, location: actualLocation }); - if (actualLocation && actualLocation.scriptId) { + if (actualLocation?.scriptId) { if (!silent) return getSourceSnippet(actualLocation, 5); } else { print(`Warning: script '${script}' was not loaded yet.`); diff --git a/lib/internal/dns/callback_resolver.js b/lib/internal/dns/callback_resolver.js index c7bf1ce1d69..fdafd310ad1 100644 --- a/lib/internal/dns/callback_resolver.js +++ b/lib/internal/dns/callback_resolver.js @@ -67,7 +67,7 @@ function resolver(bindingName) { req.callback = callback; req.hostname = name; req.oncomplete = onresolve; - req.ttl = !!(options && options.ttl); + req.ttl = !!(options?.ttl); const err = this._handle[bindingName](req, name); if (err) throw new DNSException(err, bindingName, name); if (hasObserver('dns')) { diff --git a/lib/internal/dns/promises.js b/lib/internal/dns/promises.js index 59e5252f7e1..443d21dc279 100644 --- a/lib/internal/dns/promises.js +++ b/lib/internal/dns/promises.js @@ -336,7 +336,7 @@ function resolver(bindingName) { function query(name, options) { validateString(name, 'name'); - const ttl = !!(options && options.ttl); + const ttl = !!(options?.ttl); return createResolverPromise(this, bindingName, name, ttl); } diff --git a/lib/internal/error_serdes.js b/lib/internal/error_serdes.js index 89f24eab9b1..e53291c6b77 100644 --- a/lib/internal/error_serdes.js +++ b/lib/internal/error_serdes.js @@ -85,7 +85,7 @@ function GetConstructors(object) { current !== null; current = ObjectGetPrototypeOf(current)) { const desc = ObjectGetOwnPropertyDescriptor(current, 'constructor'); - if (desc && desc.value) { + if (desc?.value) { ObjectDefineProperty(constructors, constructors.length, { __proto__: null, value: desc.value, enumerable: true, @@ -98,7 +98,7 @@ function GetConstructors(object) { function GetName(object) { const desc = ObjectGetOwnPropertyDescriptor(object, 'name'); - return desc && desc.value; + return desc?.value; } let internalUtilInspect; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 6d147800131..bcd0844ea4b 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -393,7 +393,7 @@ class ModuleLoader { if (cjsModule) { assert(finalFormat === 'commonjs-sync'); // Check if the ESM initiating import CJS is being required by the same CJS module. - if (cjsModule && cjsModule[kIsExecuting]) { + if (cjsModule?.[kIsExecuting]) { const parentFilename = urlToFilename(parentURL); let message = `Cannot import CommonJS Module ${specifier} in a cycle.`; if (parentFilename) { diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 44840ad5f38..9b89c3e1d52 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -375,7 +375,7 @@ translators.set('json', function jsonStrategy(url, source) { modulePath = isWindows ? StringPrototypeReplaceAll(pathname, '/', '\\') : pathname; module = CJSModule._cache[modulePath]; - if (module && module.loaded) { + if (module?.loaded) { const exports = module.exports; return new ModuleWrap(url, undefined, ['default'], function() { this.setExport('default', exports); @@ -389,7 +389,7 @@ translators.set('json', function jsonStrategy(url, source) { // export, we have to check again if the module already exists or not. // TODO: remove CJS loader from here as well. module = CJSModule._cache[modulePath]; - if (module && module.loaded) { + if (module?.loaded) { const exports = module.exports; return new ModuleWrap(url, undefined, ['default'], function() { this.setExport('default', exports); diff --git a/lib/internal/options.js b/lib/internal/options.js index 68acdb2a7b3..1192b46c9ed 100644 --- a/lib/internal/options.js +++ b/lib/internal/options.js @@ -17,24 +17,15 @@ let embedderOptions; // complete so that we don't accidentally include runtime-dependent // states into a runtime-independent snapshot. function getCLIOptionsFromBinding() { - if (!optionsDict) { - optionsDict = getCLIOptionsValues(); - } - return optionsDict; + return optionsDict ??= getCLIOptionsValues(); } function getCLIOptionsInfoFromBinding() { - if (!cliInfo) { - cliInfo = getCLIOptionsInfo(); - } - return cliInfo; + return cliInfo ??= getCLIOptionsInfo(); } function getEmbedderOptions() { - if (!embedderOptions) { - embedderOptions = getEmbedderOptionsFromBinding(); - } - return embedderOptions; + return embedderOptions ??= getEmbedderOptionsFromBinding(); } function refreshOptions() { @@ -42,8 +33,7 @@ function refreshOptions() { } function getOptionValue(optionName) { - const optionsDict = getCLIOptionsFromBinding(); - return optionsDict[optionName]; + return getCLIOptionsFromBinding()[optionName]; } function getAllowUnauthorized() { diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 7c1a15f3c70..aaa1dea7a8c 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -261,7 +261,7 @@ function InterfaceConstructor(input, output, completer, terminal) { function onkeypress(s, key) { self[kTtyWrite](s, key); - if (key && key.sequence) { + if (key?.sequence) { // If the key.sequence is half of a surrogate pair // (>= 0xd800 and <= 0xdfff), refresh the line so // the character is displayed appropriately. @@ -345,7 +345,7 @@ class Interface extends InterfaceConstructor { super(input, output, completer, terminal); } get columns() { - if (this.output && this.output.columns) return this.output.columns; + if (this.output?.columns) return this.output.columns; return Infinity; } diff --git a/lib/internal/source_map/source_map.js b/lib/internal/source_map/source_map.js index 926ea191cfc..c5f7c3c5918 100644 --- a/lib/internal/source_map/source_map.js +++ b/lib/internal/source_map/source_map.js @@ -269,7 +269,7 @@ class SourceMap { ArrayPrototypePush(sources, url); this.#sources[url] = true; - if (map.sourcesContent && map.sourcesContent[i]) + if (map.sourcesContent?.[i]) this.#sourceContentByURL[url] = map.sourcesContent[i]; } diff --git a/lib/internal/streams/from.js b/lib/internal/streams/from.js index aa7e031d3e4..fad5e68c10d 100644 --- a/lib/internal/streams/from.js +++ b/lib/internal/streams/from.js @@ -26,10 +26,10 @@ function from(Readable, iterable, opts) { } let isAsync; - if (iterable && iterable[SymbolAsyncIterator]) { + if (iterable?.[SymbolAsyncIterator]) { isAsync = true; iterator = iterable[SymbolAsyncIterator](); - } else if (iterable && iterable[SymbolIterator]) { + } else if (iterable?.[SymbolIterator]) { isAsync = false; iterator = iterable[SymbolIterator](); } else { diff --git a/lib/internal/streams/pipeline.js b/lib/internal/streams/pipeline.js index c3cc742a0de..4531cf06aa2 100644 --- a/lib/internal/streams/pipeline.js +++ b/lib/internal/streams/pipeline.js @@ -453,7 +453,7 @@ function pipe(src, dst, finish, finishOnlyHandleError, { end }) { if ( err && err.code === 'ERR_STREAM_PREMATURE_CLOSE' && - (rState && rState.ended && !rState.errored && !rState.errorEmitted) + (rState?.ended && !rState.errored && !rState.errorEmitted) ) { // Some readable streams will emit 'close' before 'end'. However, since // this is on the readable side 'end' should still be emitted if the diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js index 17f8e53ad56..5a6fe1db000 100644 --- a/lib/internal/streams/readable.js +++ b/lib/internal/streams/readable.js @@ -266,10 +266,10 @@ function ReadableState(options, stream, isDuplex) { // Object stream flag. Used to make read(n) ignore n and to // make all the buffer merging and length checks go away. - if (options && options.objectMode) + if (options?.objectMode) this[kState] |= kObjectMode; - if (isDuplex && options && options.readableObjectMode) + if (isDuplex && options?.readableObjectMode) this[kState] |= kObjectMode; // The point at which it stops calling _read() to fill the buffer @@ -305,7 +305,7 @@ function ReadableState(options, stream, isDuplex) { // type: null | Writable | Set. this.awaitDrainWriters = null; - if (options && options.encoding) { + if (options?.encoding) { this.decoder = new StringDecoder(options.encoding); this.encoding = options.encoding; } @@ -791,7 +791,7 @@ function onEofChunk(stream, state) { const decoder = (state[kState] & kDecoder) !== 0 ? state[kDecoderValue] : null; if (decoder) { const chunk = decoder.end(); - if (chunk && chunk.length) { + if (chunk?.length) { state.buffer.push(chunk); state.length += (state[kState] & kObjectMode) !== 0 ? 1 : chunk.length; } @@ -1461,7 +1461,7 @@ ObjectDefineProperties(Readable.prototype, { __proto__: null, enumerable: false, get: function() { - return this._readableState && this._readableState.buffer; + return this._readableState?.buffer; }, }, diff --git a/lib/internal/streams/utils.js b/lib/internal/streams/utils.js index 4b359279556..45f55316104 100644 --- a/lib/internal/streams/utils.js +++ b/lib/internal/streams/utils.js @@ -290,8 +290,7 @@ function willEmitClose(stream) { const state = wState || rState; return (!state && isServerResponse(stream)) || !!( - state && - state.autoDestroy && + state?.autoDestroy && state.emitClose && state.closed === false ); diff --git a/lib/internal/streams/writable.js b/lib/internal/streams/writable.js index 83178c2429c..b74b6088201 100644 --- a/lib/internal/streams/writable.js +++ b/lib/internal/streams/writable.js @@ -306,10 +306,10 @@ function WritableState(options, stream, isDuplex) { // instead of a V8 slot per field. this[kState] = kSync | kConstructed | kEmitClose | kAutoDestroy; - if (options && options.objectMode) + if (options?.objectMode) this[kState] |= kObjectMode; - if (isDuplex && options && options.writableObjectMode) + if (isDuplex && options?.writableObjectMode) this[kState] |= kObjectMode; // The point at which write() starts returning false @@ -1068,7 +1068,7 @@ ObjectDefineProperties(Writable.prototype, { __proto__: null, get() { const state = this._writableState; - return state && state.highWaterMark; + return state?.highWaterMark; }, }, @@ -1084,7 +1084,7 @@ ObjectDefineProperties(Writable.prototype, { __proto__: null, get() { const state = this._writableState; - return state && state.length; + return state?.length; }, }, diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index ab2c9278a83..c10db2954db 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1284,7 +1284,7 @@ function improveStack(stack, constructor, name, tag) { if (constructor === null) { const start = RegExpPrototypeExec(/^([A-Z][a-z_ A-Z0-9[\]()-]+)(?::|\n {4}at)/, stack) || RegExpPrototypeExec(/^([a-z_A-Z0-9-]*Error)$/, stack); - fallback = (start && start[1]) || ''; + fallback = (start?.[1]) || ''; len = fallback.length; fallback = fallback || 'Error'; } diff --git a/lib/net.js b/lib/net.js index 4591e3b977c..604ba648754 100644 --- a/lib/net.js +++ b/lib/net.js @@ -747,8 +747,7 @@ Socket.prototype.resetAndDestroy = function() { }; Socket.prototype.pause = function() { - if (this[kBuffer] && !this.connecting && this._handle && - this._handle.reading) { + if (this[kBuffer] && !this.connecting && this._handle?.reading) { this._handle.reading = false; if (!this.destroyed) { const err = this._handle.readStop(); @@ -1218,7 +1217,7 @@ Socket.prototype.connect = function(...args) { } // If the parent is already connecting, do not attempt to connect again - if (this._parent && this._parent.connecting) { + if (this._parent?.connecting) { return this; } @@ -2184,7 +2183,7 @@ ObjectDefineProperty(Server.prototype, 'listening', { }); Server.prototype.address = function() { - if (this._handle && this._handle.getsockname) { + if (this._handle?.getsockname) { const out = {}; const err = this._handle.getsockname(out); if (err) { diff --git a/lib/readline.js b/lib/readline.js index abc5b25d2e0..e3ae952648f 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -163,7 +163,7 @@ Interface.prototype.question[promisify.custom] = function question(query, option options = kEmptyObject; } - if (options.signal && options.signal.aborted) { + if (options.signal?.aborted) { return PromiseReject( new AbortError(undefined, { cause: options.signal.reason })); } diff --git a/lib/repl.js b/lib/repl.js index 88e2dc37d6f..005da4ee915 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -908,8 +908,8 @@ function REPLServer(prompt, StringPrototypeCharAt(trimmedCmd, 1) !== '.' && NumberIsNaN(NumberParseFloat(trimmedCmd))) { const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCmd); - const keyword = matches && matches[1]; - const rest = matches && matches[2]; + const keyword = matches?.[1]; + const rest = matches?.[2]; if (ReflectApply(_parseREPLKeyword, self, [keyword, rest]) === true) { return; } @@ -1253,7 +1253,7 @@ function filteredOwnPropertyNames(obj) { let isObjectPrototype = false; if (ObjectGetPrototypeOf(obj) === null) { const ctorDescriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor'); - if (ctorDescriptor && ctorDescriptor.value) { + if (ctorDescriptor?.value) { const ctorProto = ObjectGetPrototypeOf(ctorDescriptor.value); isObjectPrototype = ctorProto && ObjectGetPrototypeOf(ctorProto) === obj; } diff --git a/lib/timers.js b/lib/timers.js index b594f79d120..a0e58810752 100644 --- a/lib/timers.js +++ b/lib/timers.js @@ -182,7 +182,7 @@ ObjectDefineProperty(setTimeout, customPromisify, { * @returns {void} */ function clearTimeout(timer) { - if (timer && timer._onTimeout) { + if (timer?._onTimeout) { timer._onTimeout = null; unenroll(timer); return; diff --git a/lib/zlib.js b/lib/zlib.js index 6263a2fdf94..f9f859a35cd 100644 --- a/lib/zlib.js +++ b/lib/zlib.js @@ -256,7 +256,7 @@ function ZlibBase(opts, mode, handle, { flush, finishFlush, fullFlush }) { this._defaultFlushFlag = flush; this._finishFlushFlag = finishFlush; this._defaultFullFlushFlag = fullFlush; - this._info = opts && opts.info; + this._info = opts?.info; this._maxOutputLength = maxOutputLength; } ObjectSetPrototypeOf(ZlibBase.prototype, Transform.prototype); diff --git a/test/async-hooks/init-hooks.js b/test/async-hooks/init-hooks.js index 2c5167156b6..5b891879c0c 100644 --- a/test/async-hooks/init-hooks.js +++ b/test/async-hooks/init-hooks.js @@ -192,28 +192,28 @@ class ActivityCollector { _before(uid) { const h = this._getActivity(uid, 'before'); this._stamp(h, 'before'); - this._maybeLog(uid, h && h.type, 'before'); + this._maybeLog(uid, h?.type, 'before'); this.onbefore(uid); } _after(uid) { const h = this._getActivity(uid, 'after'); this._stamp(h, 'after'); - this._maybeLog(uid, h && h.type, 'after'); + this._maybeLog(uid, h?.type, 'after'); this.onafter(uid); } _destroy(uid) { const h = this._getActivity(uid, 'destroy'); this._stamp(h, 'destroy'); - this._maybeLog(uid, h && h.type, 'destroy'); + this._maybeLog(uid, h?.type, 'destroy'); this.ondestroy(uid); } _promiseResolve(uid) { const h = this._getActivity(uid, 'promiseResolve'); this._stamp(h, 'promiseResolve'); - this._maybeLog(uid, h && h.type, 'promiseResolve'); + this._maybeLog(uid, h?.type, 'promiseResolve'); this.onpromiseResolve(uid); } diff --git a/test/common/dns.js b/test/common/dns.js index d854c73629a..738f2299dd7 100644 --- a/test/common/dns.js +++ b/test/common/dns.js @@ -196,11 +196,11 @@ function writeDNSPacket(parsed) { buffers.push(new Uint16Array([ parsed.id, - parsed.flags === undefined ? kStandardResponseFlags : parsed.flags, - parsed.questions && parsed.questions.length, - parsed.answers && parsed.answers.length, - parsed.authorityAnswers && parsed.authorityAnswers.length, - parsed.additionalRecords && parsed.additionalRecords.length, + parsed.flags ?? kStandardResponseFlags, + parsed.questions?.length, + parsed.answers?.length, + parsed.authorityAnswers?.length, + parsed.additionalRecords?.length, ])); for (const q of parsed.questions) { diff --git a/test/parallel/test-cluster-setup-primary.js b/test/parallel/test-cluster-setup-primary.js index efba017fd76..32bd83fb5af 100644 --- a/test/parallel/test-cluster-setup-primary.js +++ b/test/parallel/test-cluster-setup-primary.js @@ -50,8 +50,7 @@ if (cluster.isWorker) { checks.setupEvent = true; settings = cluster.settings; - if (settings && - settings.args && settings.args[0] === 'custom argument' && + if (settings?.args && settings.args[0] === 'custom argument' && settings.silent === true && settings.exec === process.argv[1]) { checks.settingsObject = true; diff --git a/test/parallel/test-cluster-worker-exit.js b/test/parallel/test-cluster-worker-exit.js index 09e2a83701a..1503333b6b8 100644 --- a/test/parallel/test-cluster-worker-exit.js +++ b/test/parallel/test-cluster-worker-exit.js @@ -124,7 +124,7 @@ function checkResults(expected_results, results) { const expected = expected_results[k]; assert.strictEqual( - actual, expected && expected.length ? expected[0] : expected, + actual, expected?.length ? expected[0] : expected, `${expected[1] || ''} [expected: ${expected[0]} / actual: ${actual}]`); } } diff --git a/test/parallel/test-cluster-worker-kill.js b/test/parallel/test-cluster-worker-kill.js index 7307a93e1be..07ab46304f8 100644 --- a/test/parallel/test-cluster-worker-kill.js +++ b/test/parallel/test-cluster-worker-kill.js @@ -111,7 +111,7 @@ function checkResults(expected_results, results) { const expected = expected_results[k]; assert.strictEqual( - actual, expected && expected.length ? expected[0] : expected, + actual, expected?.length ? expected[0] : expected, `${expected[1] || ''} [expected: ${expected[0]} / actual: ${actual}]`); } } diff --git a/test/parallel/test-domain-throw-error-then-throw-from-uncaught-exception-handler.js b/test/parallel/test-domain-throw-error-then-throw-from-uncaught-exception-handler.js index a2afebd838f..31889c17f74 100644 --- a/test/parallel/test-domain-throw-error-then-throw-from-uncaught-exception-handler.js +++ b/test/parallel/test-domain-throw-error-then-throw-from-uncaught-exception-handler.js @@ -92,7 +92,7 @@ function createTestCmdLine(options) { testCmd += `"${process.argv[0]}"`; - if (options && options.withAbortOnUncaughtException) { + if (options?.withAbortOnUncaughtException) { testCmd += ' --abort-on-uncaught-exception'; } diff --git a/test/parallel/test-eslint-prefer-optional-chaining.js b/test/parallel/test-eslint-prefer-optional-chaining.js new file mode 100644 index 00000000000..0ce902b08ac --- /dev/null +++ b/test/parallel/test-eslint-prefer-optional-chaining.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/prefer-optional-chaining'); + +new RuleTester().run('prefer-optional-chaining', rule, { + valid: [ + { + code: 'hello?.world', + options: [] + }, + ], + invalid: [ + { + code: 'hello && hello.world', + options: [], + errors: [{ message: 'Prefer optional chaining.' }], + output: 'hello?.world' + }, + { + code: 'hello && hello.world && hello.world.foobar', + options: [], + errors: [{ message: 'Prefer optional chaining.' }], + output: 'hello?.world?.foobar' + }, + ] +}); diff --git a/test/parallel/test-module-relative-lookup.js b/test/parallel/test-module-relative-lookup.js index 1bd505392cd..675c12c541f 100644 --- a/test/parallel/test-module-relative-lookup.js +++ b/test/parallel/test-module-relative-lookup.js @@ -15,7 +15,7 @@ function testFirstInPath(moduleName, isLocalModule) { assertFunction(paths[0], '.'); paths = _module._resolveLookupPaths(moduleName, null); - assertFunction(paths && paths[0], '.'); + assertFunction(paths?.[0], '.'); } testFirstInPath('./lodash', true); diff --git a/test/parallel/test-readline-interface.js b/test/parallel/test-readline-interface.js index 4f31762bfc7..a90e07d2350 100644 --- a/test/parallel/test-readline-interface.js +++ b/test/parallel/test-readline-interface.js @@ -44,7 +44,7 @@ class FakeInput extends EventEmitter { function isWarned(emitter) { for (const name in emitter) { const listeners = emitter[name]; - if (listeners && listeners.warned) return true; + if (listeners?.warned) return true; } return false; } diff --git a/test/parallel/test-readline-promises-interface.js b/test/parallel/test-readline-promises-interface.js index b7ce0c4ff20..8e42d977301 100644 --- a/test/parallel/test-readline-promises-interface.js +++ b/test/parallel/test-readline-promises-interface.js @@ -22,7 +22,7 @@ class FakeInput extends EventEmitter { function isWarned(emitter) { for (const name in emitter) { const listeners = emitter[name]; - if (listeners && listeners.warned) return true; + if (listeners?.warned) return true; } return false; } diff --git a/test/parallel/test-stream2-large-read-stall.js b/test/parallel/test-stream2-large-read-stall.js index 2d44bb7f783..5f5618ce731 100644 --- a/test/parallel/test-stream2-large-read-stall.js +++ b/test/parallel/test-stream2-large-read-stall.js @@ -45,11 +45,11 @@ r.on('readable', function() { do { console.error(` > read(${READSIZE})`); ret = r.read(READSIZE); - console.error(` < ${ret && ret.length} (${rs.length} remain)`); + console.error(` < ${ret?.length} (${rs.length} remain)`); } while (ret && ret.length === READSIZE); console.error('<< after read()', - ret && ret.length, + ret?.length, rs.needReadable, rs.length); }); diff --git a/test/parallel/test-tls-check-server-identity.js b/test/parallel/test-tls-check-server-identity.js index fe81fc5285a..3682aee37b9 100644 --- a/test/parallel/test-tls-check-server-identity.js +++ b/test/parallel/test-tls-check-server-identity.js @@ -331,8 +331,8 @@ const tests = [ tests.forEach(function(test, i) { const err = tls.checkServerIdentity(test.host, test.cert); - assert.strictEqual(err && err.reason, + assert.strictEqual(err?.reason, test.error, `Test# ${i} failed: ${util.inspect(test)} \n` + - `${test.error} != ${(err && err.reason)}`); + `${test.error} != ${(err?.reason)}`); }); diff --git a/test/parallel/test-trace-events-dynamic-enable.js b/test/parallel/test-trace-events-dynamic-enable.js index 237bb1de8df..69251944031 100644 --- a/test/parallel/test-trace-events-dynamic-enable.js +++ b/test/parallel/test-trace-events-dynamic-enable.js @@ -38,7 +38,7 @@ async function test() { const events = []; let tracingComplete = false; session.on('NodeTracing.dataCollected', (n) => { - assert.ok(n && n.params && n.params.value); + assert.ok(n?.params?.value); events.push(...n.params.value); // append the events. }); session.on('NodeTracing.tracingComplete', () => tracingComplete = true); diff --git a/tools/doc/markdown.mjs b/tools/doc/markdown.mjs index 21104e592b8..f039391a527 100644 --- a/tools/doc/markdown.mjs +++ b/tools/doc/markdown.mjs @@ -15,7 +15,7 @@ export function replaceLinks({ filename, linksMapper }) { } }); visit(tree, 'definition', (node) => { - const htmlUrl = fileHtmlUrls && fileHtmlUrls[node.identifier]; + const htmlUrl = fileHtmlUrls?.[node.identifier]; if (htmlUrl && typeof htmlUrl === 'string') { node.url = htmlUrl; diff --git a/tools/eslint-rules/no-unescaped-regexp-dot.js b/tools/eslint-rules/no-unescaped-regexp-dot.js index 6db23732c81..6aa6d25bf20 100644 --- a/tools/eslint-rules/no-unescaped-regexp-dot.js +++ b/tools/eslint-rules/no-unescaped-regexp-dot.js @@ -93,8 +93,7 @@ module.exports = { } function checkLiteral(node) { - const isTemplate = (node.type === 'TemplateLiteral' && node.quasis && - node.quasis.length); + const isTemplate = (node.type === 'TemplateLiteral' && node.quasis?.length); if (inRegExp && (isTemplate || (typeof node.value === 'string' && node.value.length))) { let p = node.parent; @@ -108,7 +107,7 @@ module.exports = { const quasis = node.quasis; for (let i = 0; i < quasis.length; ++i) { const el = quasis[i]; - if (el.type === 'TemplateElement' && el.value && el.value.cooked) + if (el.type === 'TemplateElement' && el.value?.cooked) regexpBuffer.push([el, el.value.cooked]); } } else { diff --git a/tools/eslint-rules/prefer-optional-chaining.js b/tools/eslint-rules/prefer-optional-chaining.js new file mode 100644 index 00000000000..a460ec51bff --- /dev/null +++ b/tools/eslint-rules/prefer-optional-chaining.js @@ -0,0 +1,93 @@ +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'Prefer optional chaining', + category: 'suggestion', + }, + fixable: 'code', + schema: [], + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + // Helper function: Checks if two nodes have identical tokens + function equalTokens(left, right) { + const leftTokens = sourceCode.getTokens(left); + const rightTokens = sourceCode.getTokens(right); + return ( + leftTokens.length === rightTokens.length && + leftTokens.every((tokenL, i) => tokenL.type === rightTokens[i].type && tokenL.value === rightTokens[i].value) + ); + } + + // Check if a sequence of two nodes forms a valid member expression chain + function isValidMemberExpressionPair(left, right) { + return ( + right.type === 'MemberExpression' && + equalTokens(left, right.object) + ); + } + + // Generate the optional chaining expression + function generateOptionalChaining(ops, first, last) { + return ops.slice(first, last + 1).reduce((chain, node, i) => { + const property = node.computed ? + `[${sourceCode.getText(node.property)}]` : + sourceCode.getText(node.property); + return i === 0 ? sourceCode.getText(node) : `${chain}?.${property}`; + }, ''); + } + + return { + 'LogicalExpression[operator=&&]:exit'(node) { + // Early return if part of a larger `&&` chain + if (node.parent.type === 'LogicalExpression' && node.parent.operator === '&&') { + return; + } + + const ops = []; + let current = node; + + // Collect `&&` expressions into the ops array + while (current.type === 'LogicalExpression' && current.operator === '&&') { + ops.unshift(current.right); // Add right operand + current = current.left; + } + ops.unshift(current); // Add the leftmost operand + + // Find the first valid member expression sequence + let first = 0; + while (first < ops.length - 1 && !isValidMemberExpressionPair(ops[first], ops[first + 1])) { + first++; + } + + // No valid sequence found + if (first === ops.length - 1) return; + + context.report({ + node, + message: 'Prefer optional chaining.', + fix(fixer) { + // Find the last valid member expression sequence + let last = first; + while (last < ops.length - 1 && isValidMemberExpressionPair(ops[last], ops[last + 1])) { + last++; + } + + return fixer.replaceTextRange( + [ops[first].range[0], ops[last].range[1]], + generateOptionalChaining(ops, first, last), + ); + }, + }); + }, + }; + }, +}; diff --git a/tools/eslint-rules/rules-utils.js b/tools/eslint-rules/rules-utils.js index c5362c96cda..3c63e3e4cbc 100644 --- a/tools/eslint-rules/rules-utils.js +++ b/tools/eslint-rules/rules-utils.js @@ -100,9 +100,5 @@ module.exports.inSkipBlock = function(node) { }; function hasSkip(expression) { - return expression && - expression.callee && - (expression.callee.name === 'skip' || - expression.callee.property && - expression.callee.property.name === 'skip'); + return expression?.callee?.name === 'skip' || expression?.callee?.property?.name === 'skip'; }