1/** 2 * @fileoverview Rule to flag adding properties to native object's prototypes. 3 * @author David Nelson 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const astUtils = require("./utils/ast-utils"); 13const globals = require("globals"); 14 15//------------------------------------------------------------------------------ 16// Rule Definition 17//------------------------------------------------------------------------------ 18 19module.exports = { 20 meta: { 21 type: "suggestion", 22 23 docs: { 24 description: "disallow extending native types", 25 category: "Best Practices", 26 recommended: false, 27 url: "https://eslint.org/docs/rules/no-extend-native" 28 }, 29 30 schema: [ 31 { 32 type: "object", 33 properties: { 34 exceptions: { 35 type: "array", 36 items: { 37 type: "string" 38 }, 39 uniqueItems: true 40 } 41 }, 42 additionalProperties: false 43 } 44 ], 45 46 messages: { 47 unexpected: "{{builtin}} prototype is read only, properties should not be added." 48 } 49 }, 50 51 create(context) { 52 53 const config = context.options[0] || {}; 54 const exceptions = new Set(config.exceptions || []); 55 const modifiedBuiltins = new Set( 56 Object.keys(globals.builtin) 57 .filter(builtin => builtin[0].toUpperCase() === builtin[0]) 58 .filter(builtin => !exceptions.has(builtin)) 59 ); 60 61 /** 62 * Reports a lint error for the given node. 63 * @param {ASTNode} node The node to report. 64 * @param {string} builtin The name of the native builtin being extended. 65 * @returns {void} 66 */ 67 function reportNode(node, builtin) { 68 context.report({ 69 node, 70 messageId: "unexpected", 71 data: { 72 builtin 73 } 74 }); 75 } 76 77 /** 78 * Check to see if the `prototype` property of the given object 79 * identifier node is being accessed. 80 * @param {ASTNode} identifierNode The Identifier representing the object 81 * to check. 82 * @returns {boolean} True if the identifier is the object of a 83 * MemberExpression and its `prototype` property is being accessed, 84 * false otherwise. 85 */ 86 function isPrototypePropertyAccessed(identifierNode) { 87 return Boolean( 88 identifierNode && 89 identifierNode.parent && 90 identifierNode.parent.type === "MemberExpression" && 91 identifierNode.parent.object === identifierNode && 92 astUtils.getStaticPropertyName(identifierNode.parent) === "prototype" 93 ); 94 } 95 96 /** 97 * Check if it's an assignment to the property of the given node. 98 * Example: `*.prop = 0` // the `*` is the given node. 99 * @param {ASTNode} node The node to check. 100 * @returns {boolean} True if an assignment to the property of the node. 101 */ 102 function isAssigningToPropertyOf(node) { 103 return ( 104 node.parent.type === "MemberExpression" && 105 node.parent.object === node && 106 node.parent.parent.type === "AssignmentExpression" && 107 node.parent.parent.left === node.parent 108 ); 109 } 110 111 /** 112 * Checks if the given node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`. 113 * @param {ASTNode} node The node to check. 114 * @returns {boolean} True if the node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`. 115 */ 116 function isInDefinePropertyCall(node) { 117 return ( 118 node.parent.type === "CallExpression" && 119 node.parent.arguments[0] === node && 120 astUtils.isSpecificMemberAccess(node.parent.callee, "Object", /^definePropert(?:y|ies)$/u) 121 ); 122 } 123 124 /** 125 * Check to see if object prototype access is part of a prototype 126 * extension. There are three ways a prototype can be extended: 127 * 1. Assignment to prototype property (Object.prototype.foo = 1) 128 * 2. Object.defineProperty()/Object.defineProperties() on a prototype 129 * If prototype extension is detected, report the AssignmentExpression 130 * or CallExpression node. 131 * @param {ASTNode} identifierNode The Identifier representing the object 132 * which prototype is being accessed and possibly extended. 133 * @returns {void} 134 */ 135 function checkAndReportPrototypeExtension(identifierNode) { 136 if (!isPrototypePropertyAccessed(identifierNode)) { 137 return; // This is not `*.prototype` access. 138 } 139 140 /* 141 * `identifierNode.parent` is a MamberExpression `*.prototype`. 142 * If it's an optional member access, it may be wrapped by a `ChainExpression` node. 143 */ 144 const prototypeNode = 145 identifierNode.parent.parent.type === "ChainExpression" 146 ? identifierNode.parent.parent 147 : identifierNode.parent; 148 149 if (isAssigningToPropertyOf(prototypeNode)) { 150 151 // `*.prototype` -> MemberExpression -> AssignmentExpression 152 reportNode(prototypeNode.parent.parent, identifierNode.name); 153 } else if (isInDefinePropertyCall(prototypeNode)) { 154 155 // `*.prototype` -> CallExpression 156 reportNode(prototypeNode.parent, identifierNode.name); 157 } 158 } 159 160 return { 161 162 "Program:exit"() { 163 const globalScope = context.getScope(); 164 165 modifiedBuiltins.forEach(builtin => { 166 const builtinVar = globalScope.set.get(builtin); 167 168 if (builtinVar && builtinVar.references) { 169 builtinVar.references 170 .map(ref => ref.identifier) 171 .forEach(checkAndReportPrototypeExtension); 172 } 173 }); 174 } 175 }; 176 177 } 178}; 179