├── .gitignore ├── lib ├── index.js └── rules │ └── no-optional-call.js ├── .editorconfig ├── tests └── lib │ └── rules │ └── no-optional-call.js ├── package.json ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var path = require("path"); 4 | 5 | module.exports.rules["default"] = 6 | require( 7 | path.join(__dirname,"rules","no-optional-call.js") 8 | ); 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = tab 8 | indent_size = 4 9 | 10 | [*.md] 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /lib/rules/no-optional-call.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | meta: { 5 | type: "problem", 6 | docs: { 7 | description: "Disables the use of the optional-call operator.", 8 | recommended: true, 9 | url: "https://github.com/getify/eslint-plugin-no-optional-call", 10 | }, 11 | fixable: null, 12 | schema: [], 13 | }, 14 | 15 | create(context) { 16 | return { 17 | CallExpression(node) { 18 | if (node.optional) { 19 | context.report(node, "Avoid the use of the optional-call operator"); 20 | } 21 | }, 22 | }; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /tests/lib/rules/no-optional-call.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var path = require("path"); 4 | var rule = require( 5 | path.join("..","..","..","lib","rules","no-optional-call.js") 6 | ); 7 | var { RuleTester } = require("eslint"); 8 | 9 | var test = new RuleTester({ 10 | parser: require.resolve("@babel/eslint-parser"), 11 | parserOptions: { 12 | requireConfigFile: false, 13 | }, 14 | }); 15 | 16 | test.run("no-optional-call", rule, { 17 | valid: [ "console?.log(42);"], 18 | invalid: [ 19 | { 20 | code: "console?.log?.(42)", 21 | errors: [ 22 | { 23 | message: "Avoid the use of the optional-call operator", 24 | type: "CallExpression", 25 | }, 26 | ], 27 | }, 28 | ], 29 | }); 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-no-optional-call", 3 | "version": "1.0.3", 4 | "description": "An ESLint plugin to disable the use of the optional-call operator.", 5 | "main": "./lib/index.js", 6 | "exports": "./lib/index.js", 7 | "scripts": { 8 | "lint": "eslint .", 9 | "test": "mocha tests --recursive" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "~7.19.1", 13 | "@babel/eslint-parser": "~7.19.1", 14 | "eslint": "~8.23.1", 15 | "mocha": "^10.0.0" 16 | }, 17 | "repository": "getify/eslint-plugin-no-optional-call", 18 | "keywords": [ 19 | "eslint", 20 | "eslintplugin", 21 | "eslint-plugin", 22 | "optional" 23 | ], 24 | "bugs": { 25 | "url": "https://github.com/getify/eslint-plugin-no-optional-call/issues", 26 | "email": "getify@gmail.com" 27 | }, 28 | "author": "Kyle Simpson ", 29 | "with credits to": "Arend van Beelen jr.", 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Kyle Simpson 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESLint Plugin: no-optional-call 2 | 3 | [![npm Module](https://badge.fury.io/js/eslint-plugin-no-optional-call.svg)](https://www.npmjs.org/package/eslint-plugin-no-optional-call) 4 | 5 | ## Overview 6 | 7 | The **no-optional-call** ESLint plugin provides a single rule (non-configurable) that disallows any usage of the `?.(` optional-call form. 8 | 9 | Unlike the related `?.` / `?.[` optional-chaining operators (which are quite useful), the `?.(` optional-call operator is total junk, and should never have been added to the language -- at least, not how it was designed. 10 | 11 | This plugin allows you to ensure that it never creeps into your project by accident. 12 | 13 | ### Explanation 14 | 15 | The supposed usage of this operator is like this: 16 | 17 | ```js 18 | obj?.func?.(42); 19 | ``` 20 | 21 | The first `?.` is optional-chaining (which is fine!), but the second `?.(` is an optional-call (which is bad). But importantly, this is not necessarily an object/method feature, as the optional-call can be used with a single identifier like this: 22 | 23 | ```js 24 | func?.(42); 25 | ``` 26 | 27 | ---- 28 | 29 | **<RANT>** 30 | 31 | Let's first just focus on the syntactic design choice. The `?.(` operator uses a `.` in it, which is basically exclusively signalling a property access in **ALL THE REST OF JS**. The related `?.` and `?.[` optional-chain operators at least make sense, because those **are about accessing a property**. But not here. At least not in any way the typical JS developer would recognize. `func?.(42)` is not accessing any property, is it? I mean, if you squint, you might imagine that it's accessing the underlying "call property" of a function. But that's not a concept exposed at the JS developer level of the language (it's a nuance of specification trivia). No JS-level developer naturally thinks like that. `?(` as optional-call would have at least made a bit more sense, from a purely syntactic perspective. 32 | 33 | **</RANT>** 34 | 35 | ---- 36 | 37 | In both cases, here's the equivalent code this `?.(` operator purports to replace: 38 | 39 | ```js 40 | if (obj.func != null) { 41 | obj.func(42); 42 | } 43 | 44 | if (func != null) { 45 | func(42); 46 | } 47 | ``` 48 | 49 | The usage of `!= null` here is on purpose. It's non-nullish checking, meaning that it avoids `null` and `undefined`, but no other values. 50 | 51 | **WARNING:** a common misconception is that the `?.(` operator is actually intended for replacing *this* kind of code (which is a bit more common/relaxed): 52 | 53 | ```js 54 | obj.func && obj.func(42); 55 | 56 | // or: 57 | if (obj.func) { 58 | obj.func(42); 59 | } 60 | 61 | // ********** 62 | 63 | func && func(42); 64 | 65 | // or: 66 | if (func) { 67 | func(42); 68 | } 69 | ``` 70 | 71 | These are subtly but importantly different than the previous `!= null` forms. That's the first gotcha! If your existing code has relied on avoiding falsy values in `obj.func` / `func` other than the nullish values (`null`, `undefined`), such as `false`, `""`, `NaN`, or `0`, then switching to `?.(` *will break* your code, since the `?.(` operator only stops at `null` and `undefined` values. 72 | 73 | Oops! 74 | 75 | But here's what's worse, what really dooms this `?.(` feature, and why you should avoid ever using it in your programs (and rely on this plugin to ensure you don't). 76 | 77 | This operator *looks like* what it's doing is providing a "safe call" type of operator (which exists in other programming languages). It *seems* like it's making sure that the value is callable (is actually a function) before calling it. 78 | 79 | To the untrained eye that's not paying close attention, `?.(` looks like it *should* be doing this: 80 | 81 | ```js 82 | if (typeof obj.func == "function") { 83 | obj.func(42); 84 | } 85 | 86 | if (typeof func == "function") { 87 | func(42); 88 | } 89 | ``` 90 | 91 | But it doesn't! It only avoids `null` / `undefined` values. 92 | 93 | If `obj.func` / `func` holds *any* non-function truthy value (strings, numbers, objects, arrays, dates, regular expressions, etc), then `?.(` will attempt to execute call that value as if it was a function, which of course will fail because none of those *are* functions. 94 | 95 | In other words, *all falsy* values except `null` and `undefined`, and *all truthy values* besides functions themselves, are *all* traps where the `?.(` operator is going to fall over and break your program. 96 | 97 | ### Types!? 98 | 99 | I know a bunch of you are yelling at me that TypeScript solves this problem, because it makes sure all those other value-types are not in `obj.func` / `func`. 100 | 101 | Here's my simple rebuttal: I call utter B.S. on the design of any JS feature which is full of (technically, an infinite number of) gotcha footguns by itself, and only operates sensibly if you *also* use TypeScript. 102 | 103 | It'd be fine if TypeScript wanted to add this feature. Use it there to your heart's content! But if you're writing only JS, you should never, ever, ever, ever... use this `?.(` feature. 104 | 105 | ## Enabling The Plugin 106 | 107 | To use **no-optional-call**, load it as a plugin into ESLint and configure the rules as desired. 108 | 109 | ### `.eslintrc.json` 110 | 111 | To load the plugin and enable its rules via a local or global `.eslintrc.json` configuration file: 112 | 113 | ```json 114 | "plugins": [ 115 | "no-optional-call" 116 | ], 117 | "rules": { 118 | "no-optional-call/default": "error" 119 | } 120 | ``` 121 | 122 | ### `package.json` 123 | 124 | To load the plugin and enable its rules via a project's `package.json`: 125 | 126 | ```json 127 | "eslintConfig": { 128 | "plugins": [ 129 | "no-optional-call" 130 | ], 131 | "rules": { 132 | "no-optional-call/default": "error" 133 | } 134 | } 135 | ``` 136 | 137 | ### ESLint CLI parameters 138 | 139 | To load the plugin and enable its rules via ESLint CLI parameters, use `--plugin` and `--rule` flags: 140 | 141 | ```cmd 142 | eslint .. --plugin='no-optional-call' --rule='no-optional-call/default: error' .. 143 | ``` 144 | 145 | ### ESLint Node API 146 | 147 | To use this plugin in Node.js with the ESLint API, require the npm module, and then (for example) pass the rule's definition to `Linter#defineRule(..)`, similar to: 148 | 149 | ```js 150 | var noOptionalCall = require("eslint-plugin-no-optional-call"); 151 | 152 | // .. 153 | 154 | var eslinter = new (require("eslint").Linter)(); 155 | 156 | eslinter.defineRule("no-optional-call/default",noOptionalCall.rules.default); 157 | ``` 158 | 159 | Then lint some code like this: 160 | 161 | ```js 162 | eslinter.verify(".. some code ..",{ 163 | rules: { 164 | "no-optional-call/default": "error", 165 | } 166 | }); 167 | ``` 168 | 169 | ### Inline Comments 170 | 171 | Once the plugin is loaded, the rule can be configured using inline code comments if desired, such as: 172 | 173 | ```js 174 | /* eslint "no-optional-call/default": "error" */ 175 | ``` 176 | 177 | ## npm Package 178 | 179 | To use this plugin with a global install of ESLint (recommended): 180 | 181 | ```cmd 182 | npm install -g eslint-plugin-no-optional-call 183 | ``` 184 | 185 | To use this plugin with a local install of ESLint: 186 | 187 | ```cmd 188 | npm install eslint-plugin-no-optional-call 189 | ``` 190 | 191 | ## License 192 | 193 | All code and documentation are (c) 2022 Kyle Simpson and released under the [MIT License](http://getify.mit-license.org/). A copy of the MIT License [is also included](LICENSE.txt). 194 | 195 | NOTE: This package was heavily inspired by [eslint-plugin-no-pipe](https://github.com/arendjr/eslint-plugin-no-pipe) by [@arendjr](https://github.com/arendjr). 196 | --------------------------------------------------------------------------------