diff --git a/README.md b/README.md index 629a54f5..7d1b40df 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ $ npm install --save-dev eslint eslint-plugin-github Add `github` to your list of plugins in your ESLint config. JSON ESLint config example: + ```json { "plugins": ["github"] @@ -22,6 +23,7 @@ JSON ESLint config example: Extend the configs you wish to use. JSON ESLint config example: + ```json { "extends": ["plugin:github/recommended"] @@ -38,3 +40,23 @@ The available configs are: - Recommended rules for every application. - `typescript` - Useful rules when writing TypeScript. + +### Rules + +- [Array Foreach](./docs/rules/array-foreach.md) +- [Async Currenttarget](./docs/rules/async-currenttarget.md) +- [Async Preventdefault](./docs/rules/async-preventdefault.md) +- [Authenticity Token](./docs/rules/authenticity-token.md) +- [Get Attribute](./docs/rules/get-attribute.md) +- [JS Class Name](./docs/rules/js-class-name.md) +- [No Blur](./docs/rules/no-blur.md) +- [No D None](./docs/rules/no-d-none.md) +- [No Dataset](./docs/rules/no-dataset.md) +- [No Implicit Buggy Globals](./docs/rules/no-implicit-buggy-globals.md) +- [No Inner HTML](./docs/rules/no-inner-html.md) +- [No InnerText](./docs/rules/no-innerText.md) +- [No Then](./docs/rules/no-then.md) +- [No Useless Passive](./docs/rules/no-useless-passive.md) +- [Prefer Observers](./docs/rules/prefer-observers.md) +- [Require Passive Events](./docs/rules/require-passive-events.md) +- [Unescaped HTML Literal](./docs/rules/unescaped-html-literal.md) diff --git a/docs/rules/array-foreach.md b/docs/rules/array-foreach.md index 9d2df5b8..7df07c8a 100644 --- a/docs/rules/array-foreach.md +++ b/docs/rules/array-foreach.md @@ -1,38 +1,20 @@ -# Array.forEach +# Array Foreach Prefer `for...of` statement instead of `Array.forEach`. -```js -// bad -els.forEach(el => { - el -}) - -// good -for (const el of els) { - el -} -``` - -## Why disallow `forEach` +## Rule Details Here's a summary of why `forEach` is disallowed, and why we prefer `for...of` for almost any use-case of `forEach`: - - Allowing `forEach` encourages **layering of "bad practices"**, such as using `Array.from()` (which is less performant than using `for...of`). - - When more requirements are added on, `forEach` typically gets **chained** with other methods like `filter` or `map`, causing multiple iterations over the same Array. Encouraging `for` loops discourages chaining and encourages single-iteration logic (e.g. using a `continue` instead of `filter`). - - `for` loops are considered "more readable" and have **clearer intent**. - - `for...of` loops offer the **most flexibility** for iteration (especially vs `Array.from`). - -For more detail, here is a breakdown of each of those points: - -### Layering of bad practices +- Allowing `forEach` encourages **layering of "bad practices"**, such as using `Array.from()` (which is less performant than using `for...of`). +- When more requirements are added on, `forEach` typically gets **chained** with other methods like `filter` or `map`, causing multiple iterations over the same Array. Encouraging `for` loops discourages chaining and encourages single-iteration logic (e.g. using a `continue` instead of `filter`). +- `for` loops are considered "more readable" and have **clearer intent**. +- `for...of` loops offer the **most flexibility** for iteration (especially vs `Array.from`). Typically developers will reach for a `forEach` when they want to iterate over a set of items. However not all "iterables" have access to Array methods. So a developer might convert their iterable to an Array by using `Array.from(iter).forEach()`. This code has introduced performance problems, where a `for...of` loop would be more performant. `forEach` does not do anything special with the Array - it does not create a new array or does not aid in encapsulation (except for introducing a new lexical scope within the callback, which isn't a benefit considering we use `let`/`const`). We don't dissallow `map`/`filter`/`reduce` because they have a tangible effect - they create a new array - which would take _more_ code and be _less_ readable to do with a `for...of` loop, the exception being as more requirements are added, and we start chaining array methods together... -### Chaining - Often when using a method like `forEach` - when coming back to add new code, let's say to filter certain elements from the Array before operating on them, a developer is implicitly encouraged to use Array's method chaining to achieve this result. For example if we wanted to filter out bad apples from an Array of Apples, if the code already uses `forEach`, then its a simple addition to add `filter()`: ```diff @@ -52,22 +34,18 @@ The problem we now have is that we're iterating multiple times over the items in Chaning isn't always necessarily bad. Chaining can advertise a series of transformations that are independant from one another, and therefore aid readability. Additionally, sometimes the "goto-style" behaviour of `continue` in for loops can hamper readability. For small Arrays, performance is not going to be of concern, but caution should be applied where there is a potentially unbounded Array (such as iterating over a fetched users list) as performance can easily become a bottleneck when unchecked. -### Hiding Intent - -The `forEach` method passes more than just the current item it is iterating over. The signature of the `forEach` callback method is `(cur: T, i: Number, all: []T) => void` and it can _additionally_ override the `receiver` (`this` value), meaning that often the _intent_ of what the callback does is hidden. To put this another way, there is _no way_ to know what the following code operates on without reading the implementation: `forEach(polishApple)`. +The `forEach` method passes more than just the current item it is iterating over. The signature of the `forEach` callback method is `(cur: T, i: Number, all: []T) => void` and it can _additionally_ override the `receiver` (`this` value), meaning that often the _intent_ of what the callback does is hidden. To put this another way, there is _no way_ to know what the following code operates on without reading the implementation: `forEach(polishApple)`. The `for` loop avoids this issue. Calls are explicit within the `for` loop, as they are not passed around. For example: ```js -for(const apple of apples) { +for (const apple of apples) { polishApple(apple) } ``` We know this code can only possibly mutate `apple`, as the return value is discarded, there is no `receiver` (`this` value) as `.call()` is not used, and it cannot operate on the whole array of `apples` because it is not passed as an argument. In this respect, we can establish what the intent of `polishApple(apple)` is far more than `forEach(polishApple)`. It is too easy for `forEach` to obscure the intent. -### Flexibility - While `forEach` provides a set of arguments to the callback, it is still overall _less flexible_ than a `for` loop. A `for` loop can conditionally call the callback, can pass additional arguments to the callback (which would otherwise need to be hoisted or curried), can opt to change the `receiver` (`this` value) or not pass any `receiver` at all. This extra flexibility is the reason we almost always prefer to use `for` loops over any of the Array iteration methods. A good example of how `for` loops provide flexibility, where `forEach` constrains it, is to see how an iteration would be refactored to handle async work. Consider the following... @@ -98,7 +76,24 @@ Compare this to the `for` loop, which has a much simpler path to refactoring: } ``` +See also https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of + +👎 Examples of **incorrect** code for this rule: + +```js +els.forEach(el => { + el +}) +``` + +👍 Examples of **correct** code for this rule: + +```js +for (const el of els) { + el +} +``` -## See Also +## Version -https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of +4.3.2 diff --git a/docs/rules/async-currenttarget.md b/docs/rules/async-currenttarget.md index 832d2eae..16050d37 100644 --- a/docs/rules/async-currenttarget.md +++ b/docs/rules/async-currenttarget.md @@ -1,10 +1,22 @@ -# `event.currentTarget` in an async function +# Async Currenttarget + +## Rule Details Accessing `event.currentTarget` inside an `async function()` will likely be `null` as `currentTarget` is mutated as the event is propagated. +1. A `click` event is dispatched +2. The handler is invoked once with the expected `currentTarget` +3. An `await` defers the execution +4. The event dispatch continues, `event.currentTarget` is modified to point to the current target of another event handler and nulled out at the end of the dispatch +5. The async function resumes +6. `event.currentTarget` is now `null` + +If you're using `async`, you'll need to synchronously create a reference to `currentTarget` before any async activity. + +👎 Examples of **incorrect** code for this rule: + ```js -// bad -document.addEventListener('click', async function(event) { +document.addEventListener('click', async function (event) { // event.currentTarget will be an HTMLElement const url = event.currentTarget.getAttribute('data-url') const data = await fetch(url) @@ -15,25 +27,15 @@ document.addEventListener('click', async function(event) { }) ``` -1. A `click` event is dispatched -2. The handler is invoked once with the expected `currentTarget` -3. An `await` defers the execution -4. The event dispatch continues, `event.currentTarget` is modified to point to the current target of another event handler and nulled out at the end of the dispatch -5. The async function resumes -6. `event.currentTarget` is now `null` - -## Solutions - -If you're using `async`, you'll need to synchronously create a reference to `currentTarget` before any async activity. +👍 Examples of **correct** code for this rule: ```js -// good -document.addEventListener('click', function(event) { +document.addEventListener('click', function (event) { const currentTarget = event.currentTarget const url = currentTarget.getAttribute('data-url') // call async IIFE - ;(async function() { + ;(async function () { const data = await fetch(url) const text = currentTarget.getAttribute('data-text') @@ -45,8 +47,7 @@ document.addEventListener('click', function(event) { Alternatively, extract a function to create an element reference. ```js -// good -document.addEventListener('click', function(event) { +document.addEventListener('click', function (event) { fetchData(event.currentTarget) }) @@ -57,3 +58,7 @@ async function fetchData(el) { // ... } ``` + +## Version + +4.3.2 diff --git a/docs/rules/async-preventdefault.md b/docs/rules/async-preventdefault.md index 324e9134..4ca23ab2 100644 --- a/docs/rules/async-preventdefault.md +++ b/docs/rules/async-preventdefault.md @@ -1,16 +1,8 @@ -# `event.preventDefault()` in an async function +# Async Preventdefault Using `event.preventDefault()` inside an `async function()` won't likely work as you'd expect because synchronous nature of event dispatch. -```js -// bad -document.addEventListener('click', async function(event) { - event.preventDefault() - - const data = await fetch() - // ... -}) -``` +## Rule Details 1. A `click` event is dispatched 2. This handler is scheduled but not ran immediately because its marked async. @@ -18,13 +10,22 @@ document.addEventListener('click', async function(event) { 4. The async function is scheduled and runs. 5. Calling `preventDefault()` is now a no-op as the synchronous event dispatch has already completed. -## Solutions - If you're using `async`, you likely need to wait on a promise in the event handler. In this case you can split the event handler in two parts, one synchronous and asynchronous. +👎 Examples of **incorrect** code for this rule: + ```js -// good -document.addEventListener('click', function(event) { +document.addEventListener('click', async function (event) { + const data = await fetch() + + event.preventDefault() +}) +``` + +👍 Examples of **correct** code for this rule: + +```js +document.addEventListener('click', function (event) { // preventDefault in a regular function event.preventDefault() @@ -41,15 +42,18 @@ async function loadData(el) { This could also be done with an async IIFE. ```js -// good -document.addEventListener('click', function(event) { +document.addEventListener('click', function (event) { // preventDefault in a regular function event.preventDefault() // call async IIFE - ;(async function() { + ;(async function () { const data = await fetch() // ... })() }) ``` + +## Version + +4.3.2 diff --git a/docs/rules/authenticity-token.md b/docs/rules/authenticity-token.md index 2668f00c..ca2795d3 100644 --- a/docs/rules/authenticity-token.md +++ b/docs/rules/authenticity-token.md @@ -1,20 +1,24 @@ -# `` +# Authenticity Token + +## Rule Details The Rails `form_tag` helper creates a `
` element with a `` child element. The authenticity-token input tag contains a [Cross-Site Request Forgery (CSRF)](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29) token that is verified by the Rails app when the form is submitted. An attacker who is able to steal a user's CSRF token can perform a CSRF attack against that user. To reduce this risk, GitHub uses per-form CSRF tokens. This means that a form's method and action are embedded in that form's CSRF token. When the form is submitted, the Rails application verifies that the request's path and method match those of the CSRF token: A stolen token for the `POST /preview` endpoint will not be accepted for the `DELETE /github/github` endpoint. -## CSRF tokens in JavaScript - Requests initiated by JavaScript using XHR or Fetch still need to include a CSRF token. Prior to our use of per-form tokens, a common pattern for getting a valid CSRF token to include in a request was +Unless the JavaScript's request is for the same method/action as the form from which it takes the CSRF token, this CSRF token will _not_ be accepted by the Rails application. + +The preferred way to make an HTTP request with JavaScript is to use the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) API to serialize the input elements of a form: + +👎 Examples of **incorrect** code for this rule: + ```js const csrfToken = this.closest('form').elements['authenticity_token'].value ``` -Unless the JavaScript's request is for the same method/action as the form from which it takes the CSRF token, this CSRF token will _not_ be accepted by the Rails application. - -The preferred way to make an HTTP request with JavaScript is to use the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) API to serialize the input elements of a form: +👍 Examples of **correct** code for this rule: ```erb <%= form_tag "/my/endpoint" do %> @@ -24,13 +28,13 @@ The preferred way to make an HTTP request with JavaScript is to use the [`FormDa ``` ```js -on('click', '.js-my-button', function(e) { +on('click', '.js-my-button', function (e) { const form = this.closest('form') fetch(form.action, { method: form.method, body: new FormData(form) - }).then(function() { + }).then(function () { alert('Success!') }) @@ -45,14 +49,18 @@ An alternate, but less preferred approach is to include the a signed CSRF url in ``` ```js -on('click', '.js-my-button', function(e) { +on('click', '.js-my-button', function (e) { csrfRequest(this.getAttribute('data-url'), { method: 'PUT', body: data - }).then(function() { + }).then(function () { alert('Success!') }) e.preventDefault() }) ``` + +## Version + +4.3.2 diff --git a/docs/rules/get-attribute.md b/docs/rules/get-attribute.md index 1459db64..dda31a05 100644 --- a/docs/rules/get-attribute.md +++ b/docs/rules/get-attribute.md @@ -1,13 +1,29 @@ -# getAttribute +# Get Attribute + +## Rule Details As HTML attributes are case insensitive, prefer using lowercase. +👎 Examples of **incorrect** code for this rule: + ```js -// bad el.getAttribute('autoComplete') +``` + +```js el.getAttribute('dataFoo') +``` + +👍 Examples of **correct** code for this rule: -// good +```js el.getAttribute('autocomplete') +``` + +```js el.getAttribute('data-foo') ``` + +## Version + +4.3.2 diff --git a/docs/rules/js-class-name.md b/docs/rules/js-class-name.md index d0e42c89..f4ee5ee3 100644 --- a/docs/rules/js-class-name.md +++ b/docs/rules/js-class-name.md @@ -1,8 +1,8 @@ -# JS prefixed class names +# JS Class Name JavaScript should only query and handle events for `js-` prefixed class names. -## Statically declared +## Rule Details The key benefit is that these symbols can be easily searched for. @@ -12,20 +12,30 @@ Since its easy for humans to cross reference usage sites and implementation, so In order to trust this system, all `js-` class names MUST be statically written as string literals. This means no dynamically constructed strings by interpolation. For the same reason, `obj.send("can_#{sym}?")` makes you feel bad deep down inside, so should `querySelector("js-" + sym)`. -### Alternatives - Typically dynamically constructed `js-` classes are often mixing static symbols and user data together. Like `"js-org-#{org.login}"`. In this case, separating into a `data-` attribute would be a better solution. ```html -
+
``` Allows you to select elements by `js-org-update` and still filter by the `data-org-name` attribute if you need to. Both `js-org-update` and `data-org-name` are clearly static symbols that are easy to search for. -## Formatting - `js-` classes must start with `js-` (obviously) and only contain lowercase letters and numbers separated by `-`s. The ESLint [`github/js-class-name`](https://github.com/github/eslint-plugin-github/blob/master/lib/rules/js-class-name.js) rule enforces this style. -## See Also - [@defunkt's original proposal from 2010](https://web.archive.org/web/20180902223055/http://ozmm.org/posts/slightly_obtrusive_javascript.html). + +👎 Examples of **incorrect** code for this rule: + +```js +const el = document.querySelector('.js-Foo') +``` + +👍 Examples of **correct** code for this rule: + +```js +const el = document.querySelector('.js-foo') +``` + +## Version + +4.3.2 diff --git a/docs/rules/no-blur.md b/docs/rules/no-blur.md index 506e0f02..9e5b70ca 100644 --- a/docs/rules/no-blur.md +++ b/docs/rules/no-blur.md @@ -1,14 +1,22 @@ -# No `element.blur()` +# No Blur Do not use `element.blur()`. Blurring an element causes the focus position to be reset causing accessibility issues when using keyboard or voice navigation. Instead, restore focus by calling `element.focus()` on a prior element. +## Rule Details + +- [Use of `blur()` is discouraged by WHATWG HTML spec](https://html.spec.whatwg.org/multipage/interaction.html#dom-blur) + +👎 Examples of **incorrect** code for this rule: + ```js -// bad menu.addEventListener('close', () => { input.blur() }) +``` + +👍 Examples of **correct** code for this rule: -// good +```js menu.addEventListener('open', () => { const previouslyFocusedElement = document.activeElement @@ -20,6 +28,6 @@ menu.addEventListener('open', () => { }) ``` -## See Also +## Version -- [Use of `blur()` is discouraged by WHATWG HTML spec](https://html.spec.whatwg.org/multipage/interaction.html#dom-blur) +4.3.2 diff --git a/docs/rules/no-d-none.md b/docs/rules/no-d-none.md index 0247d90f..adc7d556 100644 --- a/docs/rules/no-d-none.md +++ b/docs/rules/no-d-none.md @@ -1,11 +1,21 @@ -# No `d-none` +# No D None + +## Rule Details Ideally JavaScript behaviors should not rely on Primer CSS when the `hidden` property can be used. -```html -