diff --git a/.eslintrc.js b/.eslintrc.js index 43e41026b2007e..fa30d90a8a31d5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -111,6 +111,16 @@ module.exports = { }, ] }, }, + { + files: [ + 'lib/internal/test_runner/**/*.js', + 'lib/internal/test_runner/**/*.cjs', + 'lib/internal/test_runner/**/*.mjs', + ], + rules: { + 'node-core/set-proto-to-null-in-object': 'error', + }, + }, ], rules: { // ESLint built-in rules diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js index 4cd7e552ce8e56..f2d3927bda7681 100644 --- a/lib/internal/test_runner/tests_stream.js +++ b/lib/internal/test_runner/tests_stream.js @@ -83,7 +83,7 @@ class TestsStream extends Readable { [kEmitMessage](type, data) { this.emit(type, data); - this.#tryPush({ type, data }); + this.#tryPush({ __proto__: null, type, data }); } #tryPush(message) { diff --git a/tools/eslint-rules/set-proto-to-null-in-object.js b/tools/eslint-rules/set-proto-to-null-in-object.js new file mode 100644 index 00000000000000..e1a6af38c032dc --- /dev/null +++ b/tools/eslint-rules/set-proto-to-null-in-object.js @@ -0,0 +1,140 @@ +'use strict'; + +module.exports = { + meta: { + messages: { + error: 'Add `__proto__: null` to object', + }, + fixable: 'code', + }, + create: function(context) { + return { + ObjectExpression(node) { + // Not adding __proto__ to module.exports as it will break a lot of libraries + if (isModuleExportsObject(node) || isModifiedExports(node)) { + return; + } + + const properties = node.properties; + let hasProto = false; + + for (const property of properties) { + + if (!property.key) { + continue; + } + + if (property.key.type === 'Identifier' && property.key.name === '__proto__') { + hasProto = true; + break; + } + + if (property.key.type === 'Literal' && property.key.value === '__proto__') { + hasProto = true; + break; + } + } + + if (hasProto) { + return; + } + + if (properties.length > 0) { + // If the object has properties but no __proto__ property + context.report({ + node, + message: 'Every object must have __proto__: null', + fix: function(fixer) { + // Generate the fix suggestion to add __proto__: null + const sourceCode = context.getSourceCode(); + const firstProperty = properties[0]; + const firstPropertyToken = sourceCode.getFirstToken(firstProperty); + + + const isMultiLine = properties.length === 1 ? + // If the object has only one property, + // it's multiline if the property is not on the same line as the object parenthesis + properties[0].loc.start.line !== node.loc.start.line : + // If the object has more than one property, + // it's multiline if the first and second properties are not on the same line + properties[0].loc.start.line !== properties[1].loc.start.line; + + const fixText = `__proto__: null,${isMultiLine ? '\n' : ' '}`; + + // Insert the fix suggestion before the first property + return fixer.insertTextBefore(firstPropertyToken, fixText); + }, + }); + } + + if (properties.length === 0) { + // If the object is empty and missing __proto__ + context.report({ + node, + message: 'Every empty object must have __proto__: null', + fix: function(fixer) { + // Generate the fix suggestion to create the object with __proto__: null + const fixText = '{ __proto__: null }'; + + // Replace the empty object with the fix suggestion + return fixer.replaceText(node, fixText); + }, + }); + } + }, + }; + }, +}; + +// Helper function to check if the object is `module.exports` +function isModuleExportsObject(node) { + return ( + node.parent && + node.parent.type === 'AssignmentExpression' && + node.parent.left && + node.parent.left.type === 'MemberExpression' && + node.parent.left.object && + node.parent.left.object.name === 'module' && + node.parent.left.property && + node.parent.left.property.name === 'exports' + ); +} + +function isModifiedExports(node) { + return ( + node.parent && + (isObjectAssignCall(node.parent) || isObjectDefinePropertiesCall(node.parent)) + ); +} + +// Helper function to check if the node represents an ObjectAssign call +function isObjectAssignCall(node) { + return ( + node.type === 'CallExpression' && + node.callee && + node.callee.type === 'Identifier' && + node.callee.name === 'ObjectAssign' && + node.arguments.length > 1 && + node.arguments.some((arg) => + arg.type === 'MemberExpression' && + arg.object.name === 'module' && + arg.property.name === 'exports', + ) + ); +} + +// Helper function to check if the node represents an ObjectDefineProperties call +function isObjectDefinePropertiesCall(node) { + return ( + node.type === 'CallExpression' && + node.callee && + node.callee.type === 'Identifier' && + node.callee.name === 'ObjectDefineProperties' && + node.arguments.length > 1 && + node.arguments.some((arg) => + arg.type === 'MemberExpression' && + arg.object.name === 'module' && + arg.property.name === 'exports', + ) + ); +}