You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							181 lines
						
					
					
						
							6.9 KiB
						
					
					
				
			
		
		
	
	
							181 lines
						
					
					
						
							6.9 KiB
						
					
					
				/** | 
						|
 * @fileoverview Rule to flag adding properties to native object's prototypes. | 
						|
 * @author David Nelson | 
						|
 */ | 
						|
 | 
						|
"use strict"; | 
						|
 | 
						|
//------------------------------------------------------------------------------ | 
						|
// Requirements | 
						|
//------------------------------------------------------------------------------ | 
						|
 | 
						|
const astUtils = require("./utils/ast-utils"); | 
						|
const globals = require("globals"); | 
						|
 | 
						|
//------------------------------------------------------------------------------ | 
						|
// Helpers | 
						|
//------------------------------------------------------------------------------ | 
						|
 | 
						|
const propertyDefinitionMethods = new Set(["defineProperty", "defineProperties"]); | 
						|
 | 
						|
//------------------------------------------------------------------------------ | 
						|
// Rule Definition | 
						|
//------------------------------------------------------------------------------ | 
						|
 | 
						|
module.exports = { | 
						|
    meta: { | 
						|
        type: "suggestion", | 
						|
 | 
						|
        docs: { | 
						|
            description: "disallow extending native types", | 
						|
            category: "Best Practices", | 
						|
            recommended: false, | 
						|
            url: "https://eslint.org/docs/rules/no-extend-native" | 
						|
        }, | 
						|
 | 
						|
        schema: [ | 
						|
            { | 
						|
                type: "object", | 
						|
                properties: { | 
						|
                    exceptions: { | 
						|
                        type: "array", | 
						|
                        items: { | 
						|
                            type: "string" | 
						|
                        }, | 
						|
                        uniqueItems: true | 
						|
                    } | 
						|
                }, | 
						|
                additionalProperties: false | 
						|
            } | 
						|
        ], | 
						|
 | 
						|
        messages: { | 
						|
            unexpected: "{{builtin}} prototype is read only, properties should not be added." | 
						|
        } | 
						|
    }, | 
						|
 | 
						|
    create(context) { | 
						|
 | 
						|
        const config = context.options[0] || {}; | 
						|
        const exceptions = new Set(config.exceptions || []); | 
						|
        const modifiedBuiltins = new Set( | 
						|
            Object.keys(globals.builtin) | 
						|
                .filter(builtin => builtin[0].toUpperCase() === builtin[0]) | 
						|
                .filter(builtin => !exceptions.has(builtin)) | 
						|
        ); | 
						|
 | 
						|
        /** | 
						|
         * Reports a lint error for the given node. | 
						|
         * @param {ASTNode} node The node to report. | 
						|
         * @param {string} builtin The name of the native builtin being extended. | 
						|
         * @returns {void} | 
						|
         */ | 
						|
        function reportNode(node, builtin) { | 
						|
            context.report({ | 
						|
                node, | 
						|
                messageId: "unexpected", | 
						|
                data: { | 
						|
                    builtin | 
						|
                } | 
						|
            }); | 
						|
        } | 
						|
 | 
						|
        /** | 
						|
         * Check to see if the `prototype` property of the given object | 
						|
         * identifier node is being accessed. | 
						|
         * @param {ASTNode} identifierNode The Identifier representing the object | 
						|
         * to check. | 
						|
         * @returns {boolean} True if the identifier is the object of a | 
						|
         * MemberExpression and its `prototype` property is being accessed, | 
						|
         * false otherwise. | 
						|
         */ | 
						|
        function isPrototypePropertyAccessed(identifierNode) { | 
						|
            return Boolean( | 
						|
                identifierNode && | 
						|
                identifierNode.parent && | 
						|
                identifierNode.parent.type === "MemberExpression" && | 
						|
                identifierNode.parent.object === identifierNode && | 
						|
                astUtils.getStaticPropertyName(identifierNode.parent) === "prototype" | 
						|
            ); | 
						|
        } | 
						|
 | 
						|
        /** | 
						|
         * Checks that an identifier is an object of a prototype whose member | 
						|
         * is being assigned in an AssignmentExpression. | 
						|
         * Example: Object.prototype.foo = "bar" | 
						|
         * @param {ASTNode} identifierNode The identifier to check. | 
						|
         * @returns {boolean} True if the identifier's prototype is modified. | 
						|
         */ | 
						|
        function isInPrototypePropertyAssignment(identifierNode) { | 
						|
            return Boolean( | 
						|
                isPrototypePropertyAccessed(identifierNode) && | 
						|
                identifierNode.parent.parent.type === "MemberExpression" && | 
						|
                identifierNode.parent.parent.parent.type === "AssignmentExpression" && | 
						|
                identifierNode.parent.parent.parent.left === identifierNode.parent.parent | 
						|
            ); | 
						|
        } | 
						|
 | 
						|
        /** | 
						|
         * Checks that an identifier is an object of a prototype whose member | 
						|
         * is being extended via the Object.defineProperty() or | 
						|
         * Object.defineProperties() methods. | 
						|
         * Example: Object.defineProperty(Array.prototype, "foo", ...) | 
						|
         * Example: Object.defineProperties(Array.prototype, ...) | 
						|
         * @param {ASTNode} identifierNode The identifier to check. | 
						|
         * @returns {boolean} True if the identifier's prototype is modified. | 
						|
         */ | 
						|
        function isInDefinePropertyCall(identifierNode) { | 
						|
            return Boolean( | 
						|
                isPrototypePropertyAccessed(identifierNode) && | 
						|
                identifierNode.parent.parent.type === "CallExpression" && | 
						|
                identifierNode.parent.parent.arguments[0] === identifierNode.parent && | 
						|
                identifierNode.parent.parent.callee.type === "MemberExpression" && | 
						|
                identifierNode.parent.parent.callee.object.type === "Identifier" && | 
						|
                identifierNode.parent.parent.callee.object.name === "Object" && | 
						|
                identifierNode.parent.parent.callee.property.type === "Identifier" && | 
						|
                propertyDefinitionMethods.has(identifierNode.parent.parent.callee.property.name) | 
						|
            ); | 
						|
        } | 
						|
 | 
						|
        /** | 
						|
         * Check to see if object prototype access is part of a prototype | 
						|
         * extension. There are three ways a prototype can be extended: | 
						|
         * 1. Assignment to prototype property (Object.prototype.foo = 1) | 
						|
         * 2. Object.defineProperty()/Object.defineProperties() on a prototype | 
						|
         * If prototype extension is detected, report the AssignmentExpression | 
						|
         * or CallExpression node. | 
						|
         * @param {ASTNode} identifierNode The Identifier representing the object | 
						|
         * which prototype is being accessed and possibly extended. | 
						|
         * @returns {void} | 
						|
         */ | 
						|
        function checkAndReportPrototypeExtension(identifierNode) { | 
						|
            if (isInPrototypePropertyAssignment(identifierNode)) { | 
						|
 | 
						|
                // Identifier --> MemberExpression --> MemberExpression --> AssignmentExpression | 
						|
                reportNode(identifierNode.parent.parent.parent, identifierNode.name); | 
						|
            } else if (isInDefinePropertyCall(identifierNode)) { | 
						|
 | 
						|
                // Identifier --> MemberExpression --> CallExpression | 
						|
                reportNode(identifierNode.parent.parent, identifierNode.name); | 
						|
            } | 
						|
        } | 
						|
 | 
						|
        return { | 
						|
 | 
						|
            "Program:exit"() { | 
						|
                const globalScope = context.getScope(); | 
						|
 | 
						|
                modifiedBuiltins.forEach(builtin => { | 
						|
                    const builtinVar = globalScope.set.get(builtin); | 
						|
 | 
						|
                    if (builtinVar && builtinVar.references) { | 
						|
                        builtinVar.references | 
						|
                            .map(ref => ref.identifier) | 
						|
                            .forEach(checkAndReportPrototypeExtension); | 
						|
                    } | 
						|
                }); | 
						|
            } | 
						|
        }; | 
						|
 | 
						|
    } | 
						|
};
 | 
						|
 |