├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── babel.config.json ├── docs └── rules │ └── no-use-extend-native.md ├── eslint.config.js ├── index.js ├── license.md ├── package.json ├── readme.md ├── src └── no-use-extend-native.js └── test └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | version: 2 6 | updates: 7 | - package-ecosystem: "npm" # See documentation for possible values 8 | directory: "/" # Location of package manifests 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | permissions: read-all 13 | 14 | concurrency: 15 | group: ${{ github.ref }}-${{ github.workflow }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | main: 20 | name: Main 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: 18 31 | 32 | - name: Install dependencies 33 | run: npm install 34 | 35 | - name: Run tests 36 | run: npm test 37 | 38 | - name: Coveralls 39 | uses: coverallsapp/github-action@v2 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | .nyc_output/ 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-syntax-import-attributes" 4 | ], 5 | "parserOpts": { 6 | "plugins": [ 7 | "@babel/plugin-syntax-import-assertions" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/rules/no-use-extend-native.md: -------------------------------------------------------------------------------- 1 | # Prevent using extended native objects 2 | 3 | ## Fail 4 | 5 | ```js 6 | 'unicorn'.green; 7 | [].customFunction(); 8 | ``` 9 | 10 | ## Pass 11 | 12 | ```js 13 | 'unicorn'.length; 14 | [].push(3); 15 | ``` 16 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import babelParser from '@babel/eslint-parser' 2 | import eslintJS from '@eslint/js' 3 | import eslintPluginEslintPlugin from 'eslint-plugin-eslint-plugin' 4 | import eslintPluginNoUseExtendNative from './index.js' 5 | import globals from 'globals' 6 | 7 | export default [ 8 | eslintJS.configs.recommended, 9 | eslintPluginEslintPlugin.configs['flat/recommended'], 10 | eslintPluginNoUseExtendNative.configs.recommended, 11 | { 12 | languageOptions: { 13 | globals: globals.node, 14 | parser: babelParser, 15 | }, 16 | ignores: [ 17 | "coverage", 18 | ], 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import rule from './src/no-use-extend-native.js' 2 | import pkg from './package.json' with {type: 'json'} 3 | 4 | const {name, version} = pkg 5 | 6 | const plugin = { 7 | meta: { 8 | name, 9 | version 10 | }, 11 | rules: { 12 | 'no-use-extend-native': rule 13 | }, 14 | configs: {}, 15 | } 16 | 17 | Object.assign(plugin.configs, { 18 | recommended: { 19 | name: 'no-use-extend-native/recommended', 20 | plugins: { 21 | 'no-use-extend-native': plugin 22 | }, 23 | rules: { 24 | 'no-use-extend-native/no-use-extend-native': 2 25 | } 26 | } 27 | }) 28 | 29 | export default plugin 30 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dustin Specker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-no-use-extend-native", 3 | "version": "0.7.2", 4 | "description": "ESLint plugin to prevent use of extended native objects", 5 | "scripts": { 6 | "lint": "eslint .", 7 | "test": "npm run lint && c8 ava" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/dustinspecker/eslint-plugin-no-use-extend-native.git" 12 | }, 13 | "bugs": "https://github.com/dustinspecker/eslint-plugin-no-use-extend-native/issues", 14 | "homepage": "https://github.com/dustinspecker/eslint-plugin-no-use-extend-native", 15 | "type": "module", 16 | "engines": { 17 | "node": ">=18.18.0" 18 | }, 19 | "keywords": [ 20 | "eslint", 21 | "eslintplugin", 22 | "eslint-plugin", 23 | "extend", 24 | "native", 25 | "prototype" 26 | ], 27 | "author": "Dustin Specker", 28 | "contributors": [ 29 | "Brett Zamir" 30 | ], 31 | "license": "MIT", 32 | "files": [ 33 | "index.js", 34 | "src" 35 | ], 36 | "exports": "./index.js", 37 | "dependencies": { 38 | "is-get-set-prop": "^2.0.0", 39 | "is-js-type": "^3.0.0", 40 | "is-obj-prop": "^2.0.0", 41 | "is-proto-prop": "^3.0.1" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.24.6", 45 | "@babel/eslint-parser": "^7.24.6", 46 | "@babel/plugin-syntax-import-assertions": "^7.24.6", 47 | "@babel/plugin-syntax-import-attributes": "^7.24.6", 48 | "@eslint/js": "^9.3.0", 49 | "ava": "^6.1.3", 50 | "c8": "^10.0.0", 51 | "eslint-ava-rule-tester": "^5.0.1", 52 | "eslint-path-formatter": "^0.1.1", 53 | "eslint-plugin-eslint-plugin": "^6.1.0", 54 | "eslint-plugin-new-with-error": "^5.0.0", 55 | "globals": "^15.3.0" 56 | }, 57 | "peerDependencies": { 58 | "eslint": "^9.3.0" 59 | }, 60 | "c8": { 61 | "reporter": [ 62 | "lcov", 63 | "text" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-no-use-extend-native 2 | [![NPM version](https://badge.fury.io/js/eslint-plugin-no-use-extend-native.svg)](https://badge.fury.io/js/eslint-plugin-no-use-extend-native) 3 | [![Coverage Status](https://coveralls.io/repos/github/dustinspecker/eslint-plugin-no-use-extend-native/badge.svg?branch=main)](https://coveralls.io/github/dustinspecker/eslint-plugin-no-use-extend-native?branch=main) 4 | 5 | > ESLint plugin to prevent use of extended native objects 6 | 7 | ## Install 8 | First, install ESLint via 9 | ``` 10 | npm install --save-dev eslint 11 | ``` 12 | 13 | Then install eslint-plugin-no-use-extend-native 14 | ``` 15 | npm install --save-dev eslint-plugin-no-use-extend-native 16 | ``` 17 | 18 | ## Usage 19 | In your `eslint.config.js` file add the plugin as such: 20 | 21 | ```javascript 22 | import eslintPluginNoUseExtendNative from 'eslint-plugin-no-use-extend-native' 23 | 24 | export default [ 25 | { 26 | plugins: { 27 | 'no-use-extend-native': eslintPluginNoUseExtendNative, 28 | }, 29 | rules: { 30 | 'no-use-extend-native/no-use-extend-native': 2, 31 | }, 32 | }, 33 | ] 34 | ``` 35 | 36 | If you want the default of the single rule being enabled as an error, you can also just use the following instead of 37 | all of the above: 38 | 39 | ```javascript 40 | import eslintPluginNoUseExtendNative from 'eslint-plugin-no-use-extend-native' 41 | 42 | export default [ 43 | eslintPluginNoUseExtendNative.configs.recommended, 44 | ] 45 | ``` 46 | 47 | With this plugin enabled, ESLint will find issues with using extended native objects: 48 | ```javascript 49 | import colors from 'colors'; 50 | console.log('unicorn'.green); 51 | // => ESLint will give an error stating 'Avoid using extended native objects' 52 | 53 | [].customFunction(); 54 | // => ESLint will give an error stating 'Avoid using extended native objects' 55 | ``` 56 | 57 | More examples can be seen in the [tests](https://github.com/dustinspecker/eslint-plugin-no-use-extend-native/blob/master/test/test.js). 58 | 59 | 60 | ## Usage with no-extend-native 61 | 62 | ESLint's [`no-extend-native`][no-extend-native] rule verifies code is not **modifying** a native prototype. e.g., with the `no-extend-native` rule enabled, the following lines are each considered incorrect: 63 | ```javascript 64 | String.prototype.shortHash = function() { return this.substring(0, 7); }; 65 | Object.defineProperty(Array.prototype, "times", { value: 999 }); 66 | ``` 67 | 68 | `no-use-extend-native` verifies code is not **using** a non-native prototype. e.g., with the `no-use-extend-native` plugin enabled, the following line is considered incorrect: 69 | ```javascript 70 | "50bda47b09923e045759db8e8dd01a0bacd97370".shortHash() === "50bda47"; 71 | ``` 72 | 73 | The `no-use-extend-native` plugin is designed to work with ESLint's `no-extend-native` rule. `no-extend-native` ensures that native prototypes aren't extended, and should a third party library extend them, `no-use-extend-native` ensures those changes aren't depended upon. 74 | 75 | [no-extend-native]: http://eslint.org/docs/rules/no-extend-native 76 | 77 | 78 | ## LICENSE 79 | MIT © [Dustin Specker](https://github.com/dustinspecker) 80 | -------------------------------------------------------------------------------- /src/no-use-extend-native.js: -------------------------------------------------------------------------------- 1 | import isGetSetProp from 'is-get-set-prop' 2 | import isJsType from 'is-js-type' 3 | import isObjProp from 'is-obj-prop' 4 | import isProtoProp from 'is-proto-prop' 5 | 6 | /** 7 | * Return type of value of left or right 8 | * @param {Object} o - left or right of node.object 9 | * @return {String} - type of o 10 | */ 11 | const getType = o => { 12 | const type = typeof o.value 13 | 14 | if (o.regex) { 15 | return 'RegExp' 16 | } 17 | 18 | return type.charAt(0).toUpperCase() + type.slice(1) 19 | } 20 | 21 | /** 22 | * Returns type of binary expression result 23 | * @param {Object} o - node's object with a BinaryExpression type 24 | * @return {String} - type of value produced 25 | */ 26 | const binaryExpressionProduces = o => { 27 | const leftType = o.left.type === 'BinaryExpression' ? binaryExpressionProduces(o.left) : getType(o.left) 28 | const rightType = o.right.type === 'BinaryExpression' ? binaryExpressionProduces(o.right) : getType(o.right) 29 | 30 | const isRegExp = leftType === rightType && leftType === 'RegExp' 31 | if (leftType === 'String' || rightType === 'String' || isRegExp) { 32 | return 'String' 33 | } 34 | 35 | if (leftType === rightType) { 36 | return leftType 37 | } 38 | 39 | return 'Unknown' 40 | } 41 | 42 | /** 43 | * Returns the JS type and property name 44 | * @param {Object} node - node to examine 45 | * @return {Object} - jsType and propertyName 46 | */ 47 | const getJsTypeAndPropertyName = node => { 48 | let propertyName, jsType 49 | 50 | switch (node.object.type) { 51 | case 'NewExpression': 52 | jsType = node.object.callee.name 53 | break 54 | case 'Literal': 55 | jsType = getType(node.object) 56 | break 57 | case 'BinaryExpression': 58 | jsType = binaryExpressionProduces(node.object) 59 | break 60 | case 'Identifier': 61 | if (node.property.name === 'prototype' && node.parent.property) { 62 | jsType = node.object.name 63 | propertyName = node.parent.property.name 64 | } else { 65 | jsType = node.object.name 66 | } 67 | break 68 | default: 69 | jsType = node.object.type.replace('Expression', '') 70 | } 71 | 72 | propertyName = propertyName || node.property.name || node.property.value 73 | 74 | return {propertyName, jsType} 75 | } 76 | 77 | const isUnkownGettSetterOrJsTypeExpressed = (jsType, propertyName, usageType) => { 78 | const isExpression = usageType === 'ExpressionStatement' || usageType === 'MemberExpression' 79 | 80 | return isExpression && !isGetSetProp(jsType, propertyName) && 81 | !isProtoProp(jsType, propertyName) && !isObjProp(jsType, propertyName) 82 | } 83 | 84 | /** 85 | * Determine if a jsType's usage of propertyName is valid 86 | * @param {String} jsType - the JS type to validate 87 | * @param {String} propertyName - the property name to validate usage of on jsType 88 | * @param {String} usageType - how propertyName is being used 89 | * @return {Boolean} - is the usage invalid? 90 | */ 91 | const isInvalid = (jsType, propertyName, usageType) => { 92 | if (typeof propertyName !== 'string' || typeof jsType !== 'string' || !isJsType(jsType)) { 93 | return false 94 | } 95 | 96 | const unknownGetterSetterOrjsTypeExpressed = isUnkownGettSetterOrJsTypeExpressed(jsType, propertyName, usageType) 97 | 98 | const isFunctionCall = usageType === 'CallExpression' 99 | const getterSetterCalledAsFunction = isFunctionCall && isGetSetProp(jsType, propertyName) 100 | 101 | const unknownjsTypeCalledAsFunction = isFunctionCall && !isProtoProp(jsType, propertyName) && 102 | !isObjProp(jsType, propertyName) 103 | 104 | return unknownGetterSetterOrjsTypeExpressed || getterSetterCalledAsFunction || unknownjsTypeCalledAsFunction 105 | } 106 | 107 | export default { 108 | meta: { 109 | type: 'problem', 110 | messages: { 111 | message: 'Avoid using extended native objects', 112 | }, 113 | schema: [], 114 | }, 115 | create(context) { 116 | return { 117 | MemberExpression(node) { 118 | /* eslint complexity: [2, 9] */ 119 | if (node.computed && node.property.type === 'Identifier') { 120 | /** 121 | * handles cases like {}[i][j] 122 | * not enough information to identify type of variable in computed properties 123 | * so ignore false positives by not performing any checks 124 | */ 125 | 126 | return 127 | } 128 | 129 | const isArgToParent = node.parent.arguments && node.parent.arguments.indexOf(node) > -1 130 | const usageType = isArgToParent ? node.type : node.parent.type 131 | 132 | const {propertyName, jsType} = getJsTypeAndPropertyName(node) 133 | 134 | if (isInvalid(jsType, propertyName, usageType) && isInvalid('Function', propertyName, usageType)) { 135 | context.report({node, messageId: 'message'}) 136 | } 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import AvaRuleTester from 'eslint-ava-rule-tester' 2 | import noUseExtendNative from '../index.js' 3 | import test from 'ava' 4 | 5 | const ruleTester = new AvaRuleTester(test) 6 | 7 | const noUseExtendNativeRule = noUseExtendNative.rules['no-use-extend-native'] 8 | 9 | ruleTester.run('no-use-extend-native/no-use-extend-native', noUseExtendNativeRule, { 10 | valid: [ 11 | 'error.plugin', 12 | 'error.plugn()', 13 | 'array.custom', 14 | 'Object.assign()', 15 | 'Object.keys', 16 | 'Object.keys()', 17 | 'gulp.task()', 18 | 'Custom.prototype.custom', 19 | 'Array.prototype.map', 20 | 'Array.prototype.map.call([1,2,3], function (x) { console.log(x) })', 21 | 'Array.apply', 22 | 'Array.call(null, 1, 2, 3)', 23 | '[].push(1)', 24 | '[][0]', 25 | '{}[i]', 26 | '{}[3]', 27 | '{}[j][k]', 28 | '({foo: {bar: 1, baz: 2}}[i][j])', 29 | '({}).toString()', 30 | '/match_this/.test()', 31 | '\'foo\'.length', 32 | '\'hi\'.padEnd', 33 | '\'hi\'.padEnd()', 34 | 'console.log(\'foo\'.length)', 35 | 'console.log(\'foo\'.toString)', 36 | 'console.log(\'foo\'.toString())', 37 | 'console.log(gulp.task)', 38 | 'console.log(gulp.task())', 39 | '\'string\'.toString()', 40 | '(1).toFixed()', 41 | '1..toFixed()', 42 | '1.0.toFixed()', 43 | '(\'str\' + \'ing\').toString()', 44 | '(\'str\' + \'i\' + \'ng\').toString()', 45 | '(1 + 1).valueOf()', 46 | '(1 + 1 + (1 + 1)).valueOf()', 47 | '(1 + 1 + 1).valueOf()', 48 | '(1 + \'string\').toString()', 49 | '(/regex/ + /regex/).toString()', 50 | '(/regex/ + 1).toString()', 51 | '([1] + [2]).toString()', 52 | '(function testFunction() {}).toString()', 53 | 'Test.prototype', 54 | 'new Array().toString()', 55 | 'new ArrayBuffer().constructor()', 56 | 'new Boolean().toString()', 57 | 'new DataView().buffer()', 58 | 'new Date().getDate()', 59 | 'new Error().message()', 60 | 'new Error().stack', 61 | 'new Error().stack.slice(1)', 62 | 'new Float32Array().values()', 63 | 'new Float64Array().values()', 64 | 'new Function().toString()', 65 | 'new Int16Array().values()', 66 | 'new Int32Array().values()', 67 | 'new Int8Array().values()', 68 | 'new Map().clear()', 69 | 'new Number().toString()', 70 | 'new Object().toString()', 71 | 'new Object().toString', 72 | 'new Promise().then()', 73 | 'new RegExp().test()', 74 | 'new Set().values()', 75 | 'new String().toString()', 76 | 'new Symbol().toString()', 77 | 'new Uint16Array().values()', 78 | 'new Uint32Array().values()', 79 | 'new Uint8ClampedArray().values()', 80 | 'new WeakMap().get()', 81 | 'new WeakSet().has()', 82 | 'new Array()[\'length\']', 83 | 'new Array()[\'toString\']()', 84 | 'Map.groupBy', 85 | 'Object.groupBy', 86 | ].map(code => ({code})), 87 | invalid: [ 88 | 'Array.prototype.custom', 89 | 'Array.to', 90 | 'Array.to()', 91 | '[].length()', 92 | '\'unicorn\'.green', 93 | '[].custom()', 94 | '({}).custom()', 95 | '/match_this/.custom()', 96 | '\'string\'.custom()', 97 | 'console.log(\'foo\'.custom)', 98 | 'console.log(\'foo\'.custom())', 99 | '(\'str\' + \'ing\').custom()', 100 | '(\'str\' + \'i\' + \'ng\').custom()', 101 | '(1 + \'ing\').custom()', 102 | '(/regex/ + \'ing\').custom()', 103 | '(1 + 1).toLowerCase()', 104 | '(1 + 1 + 1).toLowerCase()', 105 | '(function testFunction() {}).custom()', 106 | 'new Array().custom()', 107 | 'new ArrayBuffer().custom()', 108 | 'new Boolean().custom()', 109 | 'new DataView().custom()', 110 | 'new Date().custom()', 111 | 'new Error().custom()', 112 | 'new Float32Array().custom()', 113 | 'new Float64Array().custom()', 114 | 'new Function().custom()', 115 | 'new Int16Array().custom()', 116 | 'new Int32Array().custom()', 117 | 'new Int8Array().custom()', 118 | 'new Map().custom()', 119 | 'new Number().custom()', 120 | 'new Object().custom()', 121 | 'new Promise().custom()', 122 | 'new RegExp().custom()', 123 | 'new Set().custom()', 124 | 'new String().custom()', 125 | 'new Symbol().custom()', 126 | 'new Uint16Array().custom()', 127 | 'new Uint32Array().custom()', 128 | 'new Uint8Array().custom()', 129 | 'new Uint8ClampedArray().custom()', 130 | 'new WeakMap().custom()', 131 | 'new WeakSet().custom()', 132 | 'new Array()[\'custom\']', 133 | 'new Array()[\'custom\']()' 134 | ].map(code => ({ 135 | code, 136 | errors: [{message: 'Avoid using extended native objects'}] 137 | })) 138 | }) 139 | --------------------------------------------------------------------------------