diff --git a/.eslintrc.js b/.eslintrc.js index a53fedba15b..1457d9b553c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -95,12 +95,8 @@ module.exports = { files: ["lib/rules/*", "tools/internal-rules/*"], excludedFiles: ["index.js"], rules: { - "internal-rules/no-invalid-meta": "error" - - /* - * TODO: enable it when all the rules using meta.messages - * "internal-rules/consistent-meta-messages": "error" - */ + "internal-rules/no-invalid-meta": "error", + "internal-rules/consistent-meta-messages": "error" } }, { diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7a033d35a..be836630d92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,65 @@ +v7.6.0 - July 31, 2020 + +* [`ecb2b73`](https://github.com/eslint/eslint/commit/ecb2b7343a0d14fb57d297a16be6c1b176fb3dbf) Update: require `meta` for fixable rules in RuleTester (refs #13349) (#13489) (Milos Djermanovic) +* [`6fb4edd`](https://github.com/eslint/eslint/commit/6fb4edde3b7a7ae2faf8ac956a7342fbf80865fc) Docs: fix broken links in developer guide (#13518) (Sam Chen) +* [`318fe10`](https://github.com/eslint/eslint/commit/318fe103dbf2548eee293ff456ef0b829dbe3db3) Fix: Do not output `undefined` as line and column when it's unavailable (#13519) (haya14busa) +* [`493b5b4`](https://github.com/eslint/eslint/commit/493b5b40cae7a076fdeb19740f8c88fb4ae9c1fb) Sponsors: Sync README with website (ESLint Jenkins) +* [`f100143`](https://github.com/eslint/eslint/commit/f100143fa5f529aacb2b50e650a00d2697ca4c54) Sponsors: Sync README with website (ESLint Jenkins) +* [`16b10fe`](https://github.com/eslint/eslint/commit/16b10fe8ba3c78939d5ada4a25caf2f0c9e6a058) Fix: Update the chatroom link to go directly to help channel (#13536) (Nicholas C. Zakas) +* [`f937eb9`](https://github.com/eslint/eslint/commit/f937eb95407f60d3772bcb956e227aaf99e48777) Sponsors: Sync README with website (ESLint Jenkins) +* [`e71e298`](https://github.com/eslint/eslint/commit/e71e2980cd2e319afc70d8c859c7ffd59cf4157b) Update: Change no-duplicate-case to comparing tokens (fixes #13485) (#13494) (Yosuke Ota) +* [`6c4aea4`](https://github.com/eslint/eslint/commit/6c4aea44fd78e1eecea5fe3c37e1921e3b1e98a6) Docs: add ECMAScript 2020 to README (#13510) (Milos Djermanovic) + +v7.5.0 - July 18, 2020 + +* [`6ea3178`](https://github.com/eslint/eslint/commit/6ea3178776eae0e40c3f5498893e8aab0e23686b) Update: optional chaining support (fixes #12642) (#13416) (Toru Nagashima) +* [`540b1af`](https://github.com/eslint/eslint/commit/540b1af77278ae649b621aa8d4bf8d6de03c3155) Chore: enable consistent-meta-messages internal rule (#13487) (Milos Djermanovic) +* [`885a145`](https://github.com/eslint/eslint/commit/885a1455691265db88dc0befe9b48a69d69e8b9c) Docs: clarify behavior if `meta.fixable` is omitted (refs #13349) (#13493) (Milos Djermanovic) +* [`1a01b42`](https://github.com/eslint/eslint/commit/1a01b420eaab0de03dab5cc190a9f2a860c21a84) Docs: Update technology sponsors in README (#13478) (Nicholas C. Zakas) +* [`6ed9e8e`](https://github.com/eslint/eslint/commit/6ed9e8e4ff038c0259b0e7fe7ab7f4fd4ec55801) Upgrade: lodash@4.17.19 (#13499) (Yohan Siguret) +* [`45cdf00`](https://github.com/eslint/eslint/commit/45cdf00da6aeff3d584d37b0710fc8d6ad9456d6) Sponsors: Sync README with website (ESLint Jenkins) +* [`f1cc725`](https://github.com/eslint/eslint/commit/f1cc725ba1b8646dcf06a83716d96ad9bb726172) Docs: fix linebreaks between versions in changelog (#13488) (Milos Djermanovic) +* [`f4d7b9e`](https://github.com/eslint/eslint/commit/f4d7b9e1a599346b2f21ff9de003b311b51411e6) Update: deprecate id-blacklist rule (#13465) (Dimitri Mitropoulos) +* [`e14a645`](https://github.com/eslint/eslint/commit/e14a645aa495558081490f990ba221e21aa6b27c) Chore: use espree.latestEcmaVersion in fuzzer (#13484) (Milos Djermanovic) +* [`61097fe`](https://github.com/eslint/eslint/commit/61097fe5cc275d414a0c8e19b31c6060cb5568b7) Docs: Update int rule level to string (#13483) (Brandon Mills) +* [`c8f9c82`](https://github.com/eslint/eslint/commit/c8f9c8210cf4b9da8f07922093d7b219abad9f10) Update: Improve report location no-irregular-whitespace (refs #12334) (#13462) (Milos Djermanovic) +* [`f2e68ec`](https://github.com/eslint/eslint/commit/f2e68ec1d6cee6299e8a5cdf76c522c11d3008dd) Build: update webpack resolve.mainFields to match website config (#13457) (Milos Djermanovic) +* [`a96bc5e`](https://github.com/eslint/eslint/commit/a96bc5ec06f3a48bfe458bccd68d4d3b2a280ed9) Fix: arrow-body-style fixer for `in` wrap (fixes #11849) (#13228) (Anix) +* [`748734f`](https://github.com/eslint/eslint/commit/748734fdd497fbf61f3a616ff4a09169135b9396) Upgrade: Updated puppeteer version to v4.0.0 (#13444) (odidev) +* [`e951457`](https://github.com/eslint/eslint/commit/e951457b7aaa1b12b135588d36e3f4db4d7b8463) Docs: fix wording in configuring.md (#13469) (Piper) +* [`0af1d28`](https://github.com/eslint/eslint/commit/0af1d2828d27885483737867653ba1659af72005) Update: add allowSeparatedGroups option to sort-imports (fixes #12951) (#13455) (Milos Djermanovic) +* [`1050ee7`](https://github.com/eslint/eslint/commit/1050ee78a95da9484ff333dc1c74dac64c05da6f) Update: Improve report location for no-unneeded-ternary (refs #12334) (#13456) (Milos Djermanovic) +* [`b77b420`](https://github.com/eslint/eslint/commit/b77b4202bd1d5d1306f6f645e88d7a41a51715db) Update: Improve report location for max-len (refs #12334) (#13458) (Milos Djermanovic) +* [`095194c`](https://github.com/eslint/eslint/commit/095194c0fc0eb02aa69fde6b4280696e0e4de214) Fix: add end location to reports in object-curly-newline (refs #12334) (#13460) (Milos Djermanovic) +* [`10251bb`](https://github.com/eslint/eslint/commit/10251bbaeba80ac15244f385fc42cf2f2a30e5d2) Fix: add end location to reports in keyword-spacing (refs #12334) (#13461) (Milos Djermanovic) +* [`2ea7ee5`](https://github.com/eslint/eslint/commit/2ea7ee51a4e05ee76a6dae5954c3b6263b0970a3) Sponsors: Sync README with website (ESLint Jenkins) +* [`b55fd3b`](https://github.com/eslint/eslint/commit/b55fd3b8c05a29a465a794a524b06c1a28cddf0c) Sponsors: Sync README with website (ESLint Jenkins) + +v7.4.0 - July 3, 2020 + +* [`f21bad2`](https://github.com/eslint/eslint/commit/f21bad2680406a2671b877f8dba47f4475d0cc64) Docs: fix description for `never` in multiline-ternary (fixes #13368) (#13452) (Milos Djermanovic) +* [`ada2c89`](https://github.com/eslint/eslint/commit/ada2c891298382f82dfabf37cacd59a1057b2bb7) Fix: support typescript generics in arrow-parens (fixes #12570) (#13451) (Milos Djermanovic) +* [`89ee01e`](https://github.com/eslint/eslint/commit/89ee01e083f1e02293bf8d1447f9b0fdb3cb9384) Fix: Revert config cloning (fixes #13447) (#13449) (薛定谔的猫) +* [`0a463db`](https://github.com/eslint/eslint/commit/0a463dbf7cc5a77d442879c9117204d4d38db972) Docs: fix no-multiple-empty-lines examples (fixes #13432) (#13433) (Milos Djermanovic) +* [`ff5317e`](https://github.com/eslint/eslint/commit/ff5317e93425f93cfdf808609551ee67b2032543) Update: Improve array-callback-return report message (#13395) (Philip (flip) Kromer) +* [`3f51930`](https://github.com/eslint/eslint/commit/3f51930eea7cddc921a9ee3cb0328c7b649c0f83) Fix: false positive new with member in no-extra-parens (fixes #12740) (#13375) (YeonJuan) +* [`825a5b9`](https://github.com/eslint/eslint/commit/825a5b98d3d84f6eb72b75f7d8519de763cc8898) Fix: Clarify documentation on implicit ignore behavior (fixes #12348) (#12600) (Scott Hardin) +* [`c139156`](https://github.com/eslint/eslint/commit/c1391566a5f765f25716527de7b5cdee16c0ce36) Sponsors: Sync README with website (ESLint Jenkins) +* [`0c17e9d`](https://github.com/eslint/eslint/commit/0c17e9d2ac307cc288eea6ed7971bd5a7d33321a) Sponsors: Sync README with website (ESLint Jenkins) +* [`c680387`](https://github.com/eslint/eslint/commit/c680387ba61f6dccf0390d24a85d871fa83e9fea) Sponsors: Sync README with website (ESLint Jenkins) +* [`bf3939b`](https://github.com/eslint/eslint/commit/bf3939bbd9a33d0eb96cebe6a53bf61c855f9ba6) Sponsors: Sync README with website (ESLint Jenkins) +* [`7baf02e`](https://github.com/eslint/eslint/commit/7baf02e983af909800261263f125cca901a5bd0f) Sponsors: Sync README with website (ESLint Jenkins) +* [`5c4c3fd`](https://github.com/eslint/eslint/commit/5c4c3fdfbda18a13223ad36f44283adbfee8c496) Sponsors: Sync README with website (ESLint Jenkins) +* [`53912aa`](https://github.com/eslint/eslint/commit/53912aab1856327b399cca26cbb2ba81fd01bfa2) Sponsors: Sync README with website (ESLint Jenkins) +* [`51e42ec`](https://github.com/eslint/eslint/commit/51e42eca3e87d8259815d736ffe81e604f184057) Update: Add option "ignoreGlobals" to camelcase rule (fixes #11716) (#12782) (David Gasperoni) +* [`0655f66`](https://github.com/eslint/eslint/commit/0655f66525d167ca1288167b79a77087cfc8fcf6) Update: improve report location in arrow-body-style (refs #12334) (#13424) (YeonJuan) +* [`d53d69a`](https://github.com/eslint/eslint/commit/d53d69af08cfe55f42e0a0ca725b1014dabccc21) Update: prefer-regex-literal detect regex literals (fixes #12840) (#12842) (Mathias Schreck) +* [`004adae`](https://github.com/eslint/eslint/commit/004adae3f959414f56e44e5884f6221e9dcda142) Update: rename id-blacklist to id-denylist (fixes #13407) (#13408) (Kai Cataldo) + +v7.3.1 - June 22, 2020 + +* [`de77c11`](https://github.com/eslint/eslint/commit/de77c11e7515f2097ff355ddc0d7b6db9c83c892) Fix: Replace Infinity with Number.MAX_SAFE_INTEGER (fixes #13427) (#13435) (Nicholas C. Zakas) + v7.3.0 - June 19, 2020 * [`638a6d6`](https://github.com/eslint/eslint/commit/638a6d6be18b4a37cfdc7223e1f5acd3718694be) Update: add missing `additionalProperties: false` to some rules' schema (#13198) (Milos Djermanovic) @@ -22,6 +84,7 @@ v7.3.0 - June 19, 2020 * [`0f1f5ed`](https://github.com/eslint/eslint/commit/0f1f5ed2a20b8fb575d4360316861cf4c2b9b7bc) Docs: Add security policy link to README (#13403) (Nicholas C. Zakas) * [`9e9ba89`](https://github.com/eslint/eslint/commit/9e9ba897c566601cfe90522099c635ea316b235f) Sponsors: Sync README with website (ESLint Jenkins) * [`ca59fb9`](https://github.com/eslint/eslint/commit/ca59fb95a395c0a02ed23768a70e086480ab1f6d) Sponsors: Sync README with website (ESLint Jenkins) + v7.2.0 - June 5, 2020 * [`b735a48`](https://github.com/eslint/eslint/commit/b735a485e77bcc791e4c4c6b8716801d94e98b2c) Update: add enforceForFunctionPrototypeMethods option to no-extra-parens (#12895) (Milos Djermanovic) diff --git a/README.md b/README.md index 4a6aeb4d2fe..2bdd2c661eb 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [confi ### What ECMAScript versions does ESLint support? -ESLint has full support for ECMAScript 3, 5 (default), 2015, 2016, 2017, 2018, and 2019. You can set your desired ECMAScript syntax (and other settings, like global variables or your target environments) through [configuration](https://eslint.org/docs/user-guide/configuring). +ESLint has full support for ECMAScript 3, 5 (default), 2015, 2016, 2017, 2018, 2019, and 2020. You can set your desired ECMAScript syntax (and other settings, like global variables or your target environments) through [configuration](https://eslint.org/docs/user-guide/configuring). ### What about experimental features? @@ -206,6 +206,11 @@ Toru Nagashima
Kai Cataldo + + +
+Milos Djermanovic +
@@ -218,11 +223,6 @@ The people who review and implement new features.
薛定谔的猫 - - -
-Milos Djermanovic -
@@ -238,6 +238,11 @@ The people who review and fix bugs and help triage issues. Pig Fang + +
+Anix +
+
YeonJuan @@ -254,11 +259,13 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

-

Shopify Salesforce Airbnb

Silver Sponsors

+

Salesforce Airbnb Microsoft FOSS Fund Sponsorships

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Free Icons by Icons8 Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

vpn for netflix Nettikasinolista.com Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel

## Technology Sponsors * Site search ([eslint.org](https://eslint.org)) is sponsored by [Algolia](https://www.algolia.com) +* Hosting for ([eslint.org](https://eslint.org)) is sponsored by [Netlify](https://www.netlify.com) +* Password management is sponsored by [1Password](https://www.1password.com) diff --git a/docs/developer-guide/contributing/README.md b/docs/developer-guide/contributing/README.md index a22ea5d90a2..e341083febc 100644 --- a/docs/developer-guide/contributing/README.md +++ b/docs/developer-guide/contributing/README.md @@ -4,11 +4,11 @@ One of the great things about open source projects is that anyone can contribute This guide is intended for anyone who wants to contribute to an ESLint project. Please read it carefully as it answers a lot of the questions many newcomers have when first working with our projects. -## Read the [Code of Conduct](https://js.foundation/community/code-of-conduct) +## Read the [Code of Conduct](https://eslint.org/conduct) -ESLint welcomes contributions from everyone and adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct). We kindly request that you read over our code of conduct before contributing. +ESLint welcomes contributions from everyone and adheres to the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct). We kindly request that you read over our code of conduct before contributing. -## [Signing the CLA](https://js.foundation/CLA) +## [Signing the CLA](https://openjsf.org/about/the-openjs-foundation-cla/) In order to submit code or documentation to an ESLint project, you will need to electronically sign our [Contributor License Agreement](https://cla.js.foundation/eslint/eslint). The CLA is you giving us permission to use your contribution. diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md index 17058b4058a..2f47c316ccb 100644 --- a/docs/developer-guide/working-with-rules.md +++ b/docs/developer-guide/working-with-rules.md @@ -68,7 +68,7 @@ The source file for a rule exports an object with the following properties. * `fixable` (string) is either `"code"` or `"whitespace"` if the `--fix` option on the [command line](../user-guide/command-line-interface.md#fix) automatically fixes problems reported by the rule - **Important:** Without the `fixable` property, ESLint does not [apply fixes](#applying-fixes) even if the rule implements `fix` functions. Omit the `fixable` property if the rule is not fixable. + **Important:** the `fixable` property is mandatory for fixable rules. If this property isn't specified, ESLint will throw an error whenever the rule attempts to produce a fix. Omit the `fixable` property if the rule is not fixable. * `schema` (array) specifies the [options](#options-schemas) so ESLint can prevent invalid [rule configurations](../user-guide/configuring.md#configuring-rules) @@ -301,7 +301,7 @@ context.report({ Here, the `fix()` function is used to insert a semicolon after the node. Note that a fix is not immediately applied, and may not be applied at all if there are conflicts with other fixes. After applying fixes, ESLint will run all of the enabled rules again on the fixed code, potentially applying more fixes. This process will repeat up to 10 times, or until no more fixable problems are found. Afterwards, any remaining problems will be reported as usual. -**Important:** Unless the rule [exports](#rule-basics) the `meta.fixable` property, ESLint does not apply fixes even if the rule implements `fix` functions. +**Important:** The `meta.fixable` property is mandatory for fixable rules. ESLint will throw an error if a rule that implements `fix` functions does not [export](#rule-basics) the `meta.fixable` property. The `fixer` object has the following methods: @@ -733,5 +733,5 @@ The thing that makes ESLint different from other linters is the ability to defin Runtime rules are written in the same format as all other rules. Create your rule as you would any other and then follow these steps: 1. Place all of your runtime rules in the same directory (e.g., `eslint_rules`). -2. Create a [configuration file](../user-guide/configuring.md) and specify your rule ID error level under the `rules` key. Your rule will not run unless it has a value of `1` or `2` in the configuration file. +2. Create a [configuration file](../user-guide/configuring.md) and specify your rule ID error level under the `rules` key. Your rule will not run unless it has a value of `"warn"` or `"error"` in the configuration file. 3. Run the [command line interface](../user-guide/command-line-interface.md) using the `--rulesdir` option to specify the location of your runtime rules. diff --git a/docs/rules/camelcase.md b/docs/rules/camelcase.md index 96135f8c932..164dfab7c46 100644 --- a/docs/rules/camelcase.md +++ b/docs/rules/camelcase.md @@ -16,6 +16,8 @@ This rule has an object option: * `"ignoreDestructuring": true` does not check destructured identifiers (but still checks any use of those identifiers later in the code) * `"ignoreImports": false` (default) enforces camelcase style for ES2015 imports * `"ignoreImports": true` does not check ES2015 imports (but still checks any use of the imports later in the code except function arguments) +* `"ignoreGlobals": false` (default) enforces camelcase style for global variables +* `"ignoreGlobals": true` does not enforce camelcase style for global variables * `allow` (`string[]`) list of properties to accept. Accept regex. ### properties: "always" @@ -217,6 +219,28 @@ Examples of **correct** code for this rule with the `{ "ignoreImports": true }` import { snake_cased } from 'mod'; ``` +### ignoreGlobals: false + +Examples of **incorrect** code for this rule with the default `{ "ignoreGlobals": false }` option: + +```js +/*eslint camelcase: ["error", {ignoreGlobals: false}]*/ +/* global no_camelcased */ + +const foo = no_camelcased; +``` + +### ignoreGlobals: true + +Examples of **correct** code for this rule with the `{ "ignoreGlobals": true }` option: + +```js +/*eslint camelcase: ["error", {ignoreGlobals: true}]*/ +/* global no_camelcased */ + +const foo = no_camelcased; +``` + ## allow Examples of **correct** code for this rule with the `allow` option: diff --git a/docs/rules/id-blacklist.md b/docs/rules/id-blacklist.md index a7685c4c2f2..87a7d9503bb 100644 --- a/docs/rules/id-blacklist.md +++ b/docs/rules/id-blacklist.md @@ -1,82 +1,3 @@ # disallow specified identifiers (id-blacklist) -> "There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton - -Bad names can lead to hard-to-decipher code. Generic names, such as `data`, don't infer much about the code and the values it receives. This rule allows you to configure a blacklist of bad identifier names, that you don't want to see in your code. - -## Rule Details - -This rule disallows specified identifiers in assignments and `function` definitions. - -This rule will catch blacklisted identifiers that are: - -- variable declarations -- function declarations -- object properties assigned to during object creation - -It will not catch blacklisted identifiers that are: - -- function calls (so you can still use functions you do not have control over) -- object properties (so you can still use objects you do not have control over) - -## Options - -The rule takes one or more strings as options: the names of restricted identifiers. - -For example, to restrict the use of common generic identifiers: - -```json -{ - "id-blacklist": ["error", "data", "err", "e", "cb", "callback"] -} -``` - -Examples of **incorrect** code for this rule with sample `"data", "callback"` restricted identifiers: - -```js -/*eslint id-blacklist: ["error", "data", "callback"] */ - -var data = {...}; - -function callback() { - // ... -} - -element.callback = function() { - // ... -}; - -var itemSet = { - data: [...] -}; -``` - -Examples of **correct** code for this rule with sample `"data", "callback"` restricted identifiers: - -```js -/*eslint id-blacklist: ["error", "data", "callback"] */ - -var encodingOptions = {...}; - -function processFileResult() { - // ... -} - -element.successHandler = function() { - // ... -}; - -var itemSet = { - entities: [...] -}; - -callback(); // all function calls are ignored - -foo.callback(); // all function calls are ignored - -foo.data; // all property names that are not assignments are ignored -``` - -## When Not To Use It - -You can turn this rule off if you are happy for identifiers to be named freely. +This rule was **deprecated** in ESLint v7.5.0 and replaced by the [id-denylist](id-denylist.md) rule. diff --git a/docs/rules/id-denylist.md b/docs/rules/id-denylist.md new file mode 100644 index 00000000000..040f26e894c --- /dev/null +++ b/docs/rules/id-denylist.md @@ -0,0 +1,82 @@ +# disallow specified identifiers (id-denylist) + +> "There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton + +Generic names can lead to hard-to-decipher code. This rule allows you to specify a deny list of disallowed identifier names to avoid this practice. + +## Rule Details + +This rule disallows specified identifiers in assignments and `function` definitions. + +This rule will catch disallowed identifiers that are: + +- variable declarations +- function declarations +- object properties assigned to during object creation + +It will not catch disallowed identifiers that are: + +- function calls (so you can still use functions you do not have control over) +- object properties (so you can still use objects you do not have control over) + +## Options + +The rule takes one or more strings as options: the names of restricted identifiers. + +For example, to restrict the use of common generic identifiers: + +```json +{ + "id-denylist": ["error", "data", "err", "e", "cb", "callback"] +} +``` + +Examples of **incorrect** code for this rule with sample `"data", "callback"` restricted identifiers: + +```js +/*eslint id-denylist: ["error", "data", "callback"] */ + +var data = {...}; + +function callback() { + // ... +} + +element.callback = function() { + // ... +}; + +var itemSet = { + data: [...] +}; +``` + +Examples of **correct** code for this rule with sample `"data", "callback"` restricted identifiers: + +```js +/*eslint id-denylist: ["error", "data", "callback"] */ + +var encodingOptions = {...}; + +function processFileResult() { + // ... +} + +element.successHandler = function() { + // ... +}; + +var itemSet = { + entities: [...] +}; + +callback(); // all function calls are ignored + +foo.callback(); // all function calls are ignored + +foo.data; // all property names that are not assignments are ignored +``` + +## When Not To Use It + +You can turn this rule off if you do not want to restrict the use of certain identifiers. diff --git a/docs/rules/multiline-ternary.md b/docs/rules/multiline-ternary.md index e9461a1ca08..0c38bf7d3f1 100644 --- a/docs/rules/multiline-ternary.md +++ b/docs/rules/multiline-ternary.md @@ -27,7 +27,7 @@ This rule has a string option: * `"always"` (default) enforces newlines between the operands of a ternary expression. * `"always-multiline"` enforces newlines between the operands of a ternary expression if the expression spans multiple lines. -* `"never"` disallows newlines between the operands of a ternary expression (enforcing that the entire ternary expression is on one line). +* `"never"` disallows newlines between the operands of a ternary expression. ### always @@ -134,6 +134,10 @@ Examples of **correct** code for this rule with the `"never"` option: foo > bar ? value1 : value2; foo > bar ? (baz > qux ? value1 : value2) : value3; + +foo > bar ? ( + baz > qux ? value1 : value2 +) : value3; ``` ## When Not To Use It diff --git a/docs/rules/no-multiple-empty-lines.md b/docs/rules/no-multiple-empty-lines.md index 836cb18e471..aff6c2e6f1b 100644 --- a/docs/rules/no-multiple-empty-lines.md +++ b/docs/rules/no-multiple-empty-lines.md @@ -23,6 +23,8 @@ Examples of **incorrect** code for this rule with the default `{ "max": 2 }` opt var foo = 5; + + var bar = 3; ``` @@ -33,6 +35,7 @@ Examples of **correct** code for this rule with the default `{ "max": 2 }` optio var foo = 5; + var bar = 3; ``` @@ -45,7 +48,10 @@ Examples of **incorrect** code for this rule with the `{ max: 2, maxEOF: 0 }` op var foo = 5; + var bar = 3; + + ``` Examples of **correct** code for this rule with the `{ max: 2, maxEOF: 0 }` options: @@ -55,6 +61,7 @@ Examples of **correct** code for this rule with the `{ max: 2, maxEOF: 0 }` opti var foo = 5; + var bar = 3; ``` @@ -92,8 +99,10 @@ Examples of **incorrect** code for this rule with the `{ max: 2, maxBOF: 1 }` op ```js /*eslint no-multiple-empty-lines: ["error", { "max": 2, "maxBOF": 1 }]*/ + var foo = 5; + var bar = 3; ``` @@ -104,6 +113,7 @@ Examples of **correct** code for this rule with the `{ max: 2, maxBOF: 1 }` opti var foo = 5; + var bar = 3; ``` diff --git a/docs/rules/prefer-regex-literals.md b/docs/rules/prefer-regex-literals.md index fea589d3412..2ba8cacc08d 100644 --- a/docs/rules/prefer-regex-literals.md +++ b/docs/rules/prefer-regex-literals.md @@ -88,6 +88,38 @@ RegExp(`${prefix}abc`); new RegExp(String.raw`^\d\. ${suffix}`); ``` +## Options + +This rule has an object option: + +* `disallowRedundantWrapping` set to `true` additionally checks for unnecessarily wrapped regex literals (Default `false`). + +### `disallowRedundantWrapping` + +By default, this rule doesn’t check when a regex literal is unnecessarily wrapped in a `RegExp` constructor call. When the option `disallowRedundantWrapping` is set to `true`, the rule will also disallow such unnecessary patterns. + +Examples of `incorrect` code for `{ "disallowRedundantWrapping": true }` + +```js +/*eslint prefer-regex-literals: ["error", {"disallowRedundantWrapping": true}]*/ + +new RegExp(/abc/); + +new RegExp(/abc/, 'u'); +``` + +Examples of `correct` code for `{ "disallowRedundantWrapping": true }` + +```js +/*eslint prefer-regex-literals: ["error", {"disallowRedundantWrapping": true}]*/ + +/abc/; + +/abc/u; + +new RegExp(/abc/, flags); +``` + ## Further Reading * [MDN: Regular Expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) diff --git a/docs/rules/sort-imports.md b/docs/rules/sort-imports.md index f6fd04f0683..637c9fcf3f3 100644 --- a/docs/rules/sort-imports.md +++ b/docs/rules/sort-imports.md @@ -41,6 +41,7 @@ This rule accepts an object with its properties as * `all` = import all members provided by exported bindings. * `multiple` = import multiple members. * `single` = import single member. +* `allowSeparatedGroups` (default: `false`) Default option settings are: @@ -50,7 +51,8 @@ Default option settings are: "ignoreCase": false, "ignoreDeclarationSort": false, "ignoreMemberSort": false, - "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] + "memberSyntaxSortOrder": ["none", "all", "multiple", "single"], + "allowSeparatedGroups": false }] } ``` @@ -226,6 +228,53 @@ import {a, b} from 'foo.js'; Default is `["none", "all", "multiple", "single"]`. +### `allowSeparatedGroups` + +When `true` the rule checks the sorting of import declaration statements only for those that appear on consecutive lines. + +In other words, a blank line or a comment line or line with any other statement after an import declaration statement will reset the sorting of import declaration statements. + +Examples of **incorrect** code for this rule with the `{ "allowSeparatedGroups": true }` option: + +```js +/*eslint sort-imports: ["error", { "allowSeparatedGroups": true }]*/ + +import b from 'foo.js'; +import c from 'bar.js'; +import a from 'baz.js'; +``` + +Examples of **correct** code for this rule with the `{ "allowSeparatedGroups": true }` option: + +```js +/*eslint sort-imports: ["error", { "allowSeparatedGroups": true }]*/ + +import b from 'foo.js'; +import c from 'bar.js'; + +import a from 'baz.js'; +``` + +```js +/*eslint sort-imports: ["error", { "allowSeparatedGroups": true }]*/ + +import b from 'foo.js'; +import c from 'bar.js'; +// comment +import a from 'baz.js'; +``` + +```js +/*eslint sort-imports: ["error", { "allowSeparatedGroups": true }]*/ + +import b from 'foo.js'; +import c from 'bar.js'; +quux(); +import a from 'baz.js'; +``` + +Default is `false`. + ## When Not To Use It This rule is a formatting preference and not following it won't negatively affect the quality of your code. If alphabetizing imports isn't a part of your coding standards, then you can leave this rule disabled. diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index 7f880198bf4..50a0341fd06 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -91,7 +91,7 @@ To specify processors in a configuration file, use the `processor` key with the } ``` -To specify processors for a specific kind of files, use the combination of the `overrides` key and the `processor` key. For example, the following uses the processor `a-plugin/markdown` for `*.md` files. +To specify processors for specific kinds of files, use the combination of the `overrides` key and the `processor` key. For example, the following uses the processor `a-plugin/markdown` for `*.md` files. ```json { @@ -1070,18 +1070,36 @@ Of particular note is that like `.gitignore` files, all paths used as patterns f Please see `.gitignore`'s specification for further examples of valid syntax. -In addition to any patterns in a `.eslintignore` file, ESLint ignores files in `/**/node_modules/*` by default. It can still be added using `!`. +In addition to any patterns in the `.eslintignore` file, ESLint always follows a couple implicit ignore rules even if the `--no-ignore` flag is passed. The implicit rules are as follows: -For example, placing the following `.eslintignore` file in the current working directory will not ignore `node_modules/*` and ignore anything in the `build/` directory except `build/index.js`: +* `node_modules/` is ignored. +* Dotfiles (except for `.eslintrc.*`) as well as Dotfolders and their contents are ignored. -```text -# node_modules/* is ignored by default, but can be added using ! -!node_modules/* +There are also some exceptions to these rules: -# Ignore built files except build/index.js -build/* -!build/index.js -``` +* If the path to lint is a glob pattern or directory path and contains a Dotfolder, all Dotfiles and Dotfolders will be linted. This includes sub-dotfiles and sub-dotfolders that are buried deeper in the directory structure. + + For example, `eslint .config/` will lint all Dotfolders and Dotfiles in the `.config` directory, including immediate children as well as children that are deeper in the directory structure. + +* If the path to lint is a specific file path and the `--no-ignore` flag has been passed, ESLint will lint the file regardless of the implicit ignore rules. + + For example, `eslint .config/my-config-file.js --no-ignore` will cause `my-config-file.js` to be linted. It should be noted that the same command without the `--no-ignore` line will not lint the `my-config-file.js` file. + +* Allowlist and denylist rules specified via `--ignore-pattern` or `.eslintignore` are prioritized above implicit ignore rules. + + For example, in this scenario, `.build/test.js` is the desired file to allowlist. Because all Dotfolders and their children are ignored by default, `.build` must first be allowlisted so that eslint because aware of its children. Then, `.build/test.js` must be explicitly allowlisted, while the rest of the content is denylisted. This is done with the following `.eslintignore` file: + + ```text + # Allowlist 'test.js' in the '.build' folder + # But do not allow anything else in the '.build' folder to be linted + !.build + .build/* + !.build/test.js + ``` + + The following `--ignore-pattern` is also equivalent: + + eslint --ignore-pattern '!.build' --ignore-pattern '.build/*' --ignore-pattern '!.build/test.js' parent-folder/ ### Using an Alternate File @@ -1127,13 +1145,28 @@ You'll see this warning: ```text foo.js - 0:0 warning File ignored because of your .eslintignore file. Use --no-ignore to override. + 0:0 warning File ignored because of a matching ignore pattern. Use "--no-ignore" to override. ✖ 1 problem (0 errors, 1 warning) ``` This message occurs because ESLint is unsure if you wanted to actually lint the file or not. As the message indicates, you can use `--no-ignore` to omit using the ignore rules. +Consider another scenario where you may want to run ESLint on a specific Dotfile or Dotfolder, but have forgotten to specifically allow those files in your `.eslintignore` file. You would run something like this: + + eslint .config/foo.js + +You would see this warning: + +```text +.config/foo.js + 0:0 warning File ignored by default. Use a negated ignore pattern (like "--ignore-pattern '!'") to override + +✖ 1 problem (0 errors, 1 warning) +``` + +This messages occurs because, normally, this file would be ignored by ESLint's implicit ignore rules (as mentioned above). A negated ignore rule in your `.eslintignore` file would override the implicit rule and reinclude this file for linting. Additionally, in this specific case, `--no-ignore` could be used to lint the file as well. + ## Personal Configuration File (deprecated) ⚠️ **This feature has been deprecated**. This feature will be removed in the 8.0.0 release. If you want to continue to use personal configuration files, please use the [`--config` CLI option](https://eslint.org/docs/user-guide/command-line-interface#-c---config). For more information regarding this decision, please see [RFC 28](https://github.com/eslint/rfcs/pull/28) and [RFC 32](https://github.com/eslint/rfcs/pull/32). diff --git a/karma.conf.js b/karma.conf.js index dfc6bab4899..974c8972136 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,6 +1,13 @@ "use strict"; +const os = require("os"); -process.env.CHROME_BIN = require("puppeteer").executablePath(); +if (os.arch() === "arm64") { + + // For arm64 architecture, install chromium-browser using "apt-get install chromium-browser" + process.env.CHROME_BIN = "/usr/bin/chromium-browser"; +} else { + process.env.CHROME_BIN = require("puppeteer").executablePath(); +} module.exports = function(config) { config.set({ diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index 6d0992151ad..7c0fba65c67 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -697,33 +697,6 @@ class ConfigArrayFactory { ctx.matchBasePath ); - /** - * Cloning the rule's config as we are setting `useDefaults` to true` - * which mutates the config with its default value if present. And when we - * refer to a same variable for config for different rules, that referred variable will - * be mutated and it will be used for both. - * - * Example: - * - * const commonRuleConfig = ['error', {}]; - * - * Now if we use this variable as a config for rules like this - * - * { - * rules: { - * "a" : commonRuleConfig, - * "b" : commonRuleConfig, - * } - * } - * - * And if these rules have default values in their schema, their - * config will be mutated with default values, the mutated `commonRuleConfig` will be used for `b` as well and it probably - * throw schema voilation errors. - * - * Refer https://github.com/eslint/eslint/issues/12592 - */ - const clonedRulesConfig = rules && JSON.parse(JSON.stringify((rules))); - // Flatten `extends`. for (const extendName of extendList.filter(Boolean)) { yield* this._loadExtends(extendName, ctx); @@ -758,7 +731,7 @@ class ConfigArrayFactory { processor, reportUnusedDisableDirectives, root, - rules: clonedRulesConfig, + rules, settings }; diff --git a/lib/cli-engine/formatters/checkstyle.js b/lib/cli-engine/formatters/checkstyle.js index ba4d1b5b3ec..f19b6fc0957 100644 --- a/lib/cli-engine/formatters/checkstyle.js +++ b/lib/cli-engine/formatters/checkstyle.js @@ -42,8 +42,8 @@ module.exports = function(results) { messages.forEach(message => { output += [ - `` diff --git a/lib/linter/code-path-analysis/code-path-analyzer.js b/lib/linter/code-path-analysis/code-path-analyzer.js index b612cf43566..3a0cda51118 100644 --- a/lib/linter/code-path-analysis/code-path-analyzer.js +++ b/lib/linter/code-path-analysis/code-path-analyzer.js @@ -244,6 +244,19 @@ function preprocess(analyzer, node) { const parent = node.parent; switch (parent.type) { + + // The `arguments.length == 0` case is in `postprocess` function. + case "CallExpression": + if (parent.optional === true && parent.arguments.length >= 1 && parent.arguments[0] === node) { + state.makeOptionalRight(); + } + break; + case "MemberExpression": + if (parent.optional === true && parent.property === node) { + state.makeOptionalRight(); + } + break; + case "LogicalExpression": if ( parent.right === node && @@ -377,6 +390,20 @@ function processCodePathToEnter(analyzer, node) { analyzer.emitter.emit("onCodePathStart", codePath, node); break; + case "ChainExpression": + state.pushChainContext(); + break; + case "CallExpression": + if (node.optional === true) { + state.makeOptionalNode(); + } + break; + case "MemberExpression": + if (node.optional === true) { + state.makeOptionalNode(); + } + break; + case "LogicalExpression": if (isHandledLogicalOperator(node.operator)) { state.pushChoiceContext( @@ -449,6 +476,10 @@ function processCodePathToExit(analyzer, node) { let dontForward = false; switch (node.type) { + case "ChainExpression": + state.popChainContext(); + break; + case "IfStatement": case "ConditionalExpression": state.popChoiceContext(); @@ -583,6 +614,13 @@ function postprocess(analyzer, node) { break; } + // The `arguments.length >= 1` case is in `preprocess` function. + case "CallExpression": + if (node.optional === true && node.arguments.length === 0) { + CodePath.getState(analyzer.codePath).makeOptionalRight(); + } + break; + default: break; } diff --git a/lib/linter/code-path-analysis/code-path-segment.js b/lib/linter/code-path-analysis/code-path-segment.js index 6b17b25c7fd..ca96ad34189 100644 --- a/lib/linter/code-path-analysis/code-path-segment.js +++ b/lib/linter/code-path-analysis/code-path-segment.js @@ -92,7 +92,6 @@ class CodePathSegment { /* istanbul ignore if */ if (debug.enabled) { this.internal.nodes = []; - this.internal.exitNodes = []; } } diff --git a/lib/linter/code-path-analysis/code-path-state.js b/lib/linter/code-path-analysis/code-path-state.js index 9e760601a0f..f2b16d07e0d 100644 --- a/lib/linter/code-path-analysis/code-path-state.js +++ b/lib/linter/code-path-analysis/code-path-state.js @@ -234,6 +234,7 @@ class CodePathState { this.tryContext = null; this.loopContext = null; this.breakContext = null; + this.chainContext = null; this.currentSegments = []; this.initialSegment = this.forkContext.head[0]; @@ -555,6 +556,64 @@ class CodePathState { ); } + //-------------------------------------------------------------------------- + // ChainExpression + //-------------------------------------------------------------------------- + + /** + * Push a new `ChainExpression` context to the stack. + * This method is called on entering to each `ChainExpression` node. + * This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node. + * @returns {void} + */ + pushChainContext() { + this.chainContext = { + upper: this.chainContext, + countChoiceContexts: 0 + }; + } + + /** + * Pop a `ChainExpression` context from the stack. + * This method is called on exiting from each `ChainExpression` node. + * This merges all forks of the last optional chaining. + * @returns {void} + */ + popChainContext() { + const context = this.chainContext; + + this.chainContext = context.upper; + + // pop all choice contexts of this. + for (let i = context.countChoiceContexts; i > 0; --i) { + this.popChoiceContext(); + } + } + + /** + * Create a choice context for optional access. + * This method is called on entering to each `(Call|Member)Expression[optional=true]` node. + * This creates a choice context as similar to `LogicalExpression[operator="??"]` node. + * @returns {void} + */ + makeOptionalNode() { + if (this.chainContext) { + this.chainContext.countChoiceContexts += 1; + this.pushChoiceContext("??", false); + } + } + + /** + * Create a fork. + * This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node. + * @returns {void} + */ + makeOptionalRight() { + if (this.chainContext) { + this.makeLogicalRight(); + } + } + //-------------------------------------------------------------------------- // SwitchStatement //-------------------------------------------------------------------------- diff --git a/lib/linter/code-path-analysis/debug-helpers.js b/lib/linter/code-path-analysis/debug-helpers.js index bde4e0a38ad..a4cb99a22e0 100644 --- a/lib/linter/code-path-analysis/debug-helpers.js +++ b/lib/linter/code-path-analysis/debug-helpers.js @@ -25,6 +25,22 @@ function getId(segment) { // eslint-disable-line jsdoc/require-jsdoc return segment.id + (segment.reachable ? "" : "!"); } +/** + * Get string for the given node and operation. + * @param {ASTNode} node The node to convert. + * @param {"enter" | "exit" | undefined} label The operation label. + * @returns {string} The string representation. + */ +function nodeToString(node, label) { + const suffix = label ? `:${label}` : ""; + + switch (node.type) { + case "Identifier": return `${node.type}${suffix} (${node.name})`; + case "Literal": return `${node.type}${suffix} (${node.value})`; + default: return `${node.type}${suffix}`; + } +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -56,9 +72,15 @@ module.exports = { const segInternal = state.currentSegments[i].internal; if (leaving) { - segInternal.exitNodes.push(node); + const last = segInternal.nodes.length - 1; + + if (last >= 0 && segInternal.nodes[last] === nodeToString(node, "enter")) { + segInternal.nodes[last] = nodeToString(node, void 0); + } else { + segInternal.nodes.push(nodeToString(node, "exit")); + } } else { - segInternal.nodes.push(node); + segInternal.nodes.push(nodeToString(node, "enter")); } } @@ -104,23 +126,8 @@ module.exports = { text += "style=\"rounded,dashed,filled\",fillcolor=\"#FF9800\",label=\"<>\\n"; } - if (segment.internal.nodes.length > 0 || segment.internal.exitNodes.length > 0) { - text += [].concat( - segment.internal.nodes.map(node => { - switch (node.type) { - case "Identifier": return `${node.type} (${node.name})`; - case "Literal": return `${node.type} (${node.value})`; - default: return node.type; - } - }), - segment.internal.exitNodes.map(node => { - switch (node.type) { - case "Identifier": return `${node.type}:exit (${node.name})`; - case "Literal": return `${node.type}:exit (${node.value})`; - default: return `${node.type}:exit`; - } - }) - ).join("\\n"); + if (segment.internal.nodes.length > 0) { + text += segment.internal.nodes.join("\\n"); } else { text += "????"; } diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 3e391576716..905f3418121 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -861,6 +861,16 @@ class RuleTester { ); } + // Rules that produce fixes must have `meta.fixable` property. + if (result.output !== item.code) { + assert.ok( + hasOwnProperty(rule, "meta"), + "Fixable rules should export a `meta.fixable` property." + ); + + // Linter throws if a rule that produced a fix has `meta` but doesn't have `meta.fixable`. + } + assertASTDidntChange(result.beforeAST, result.afterAST); } diff --git a/lib/rules/accessor-pairs.js b/lib/rules/accessor-pairs.js index cf994ad2574..0e0d07a00c9 100644 --- a/lib/rules/accessor-pairs.js +++ b/lib/rules/accessor-pairs.js @@ -86,16 +86,6 @@ function isAccessorKind(node) { return node.kind === "get" || node.kind === "set"; } -/** - * Checks whether or not a given node is an `Identifier` node which was named a given name. - * @param {ASTNode} node A node to check. - * @param {string} name An expected name of the node. - * @returns {boolean} `true` if the node is an `Identifier` node which was named as expected. - */ -function isIdentifier(node, name) { - return node.type === "Identifier" && node.name === name; -} - /** * Checks whether or not a given node is an argument of a specified method call. * @param {ASTNode} node A node to check. @@ -109,10 +99,7 @@ function isArgumentOfMethodCall(node, index, object, property) { return ( parent.type === "CallExpression" && - parent.callee.type === "MemberExpression" && - parent.callee.computed === false && - isIdentifier(parent.callee.object, object) && - isIdentifier(parent.callee.property, property) && + astUtils.isSpecificMemberAccess(parent.callee, object, property) && parent.arguments[index] === node ); } diff --git a/lib/rules/array-callback-return.js b/lib/rules/array-callback-return.js index 62ba7b72d87..7267347149d 100644 --- a/lib/rules/array-callback-return.js +++ b/lib/rules/array-callback-return.js @@ -9,8 +9,6 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"); - const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ @@ -30,17 +28,27 @@ function isReachable(segment) { } /** - * Checks a given node is a MemberExpression node which has the specified name's + * Checks a given node is a member access which has the specified name's * property. * @param {ASTNode} node A node to check. - * @returns {boolean} `true` if the node is a MemberExpression node which has - * the specified name's property + * @returns {boolean} `true` if the node is a member access which has + * the specified name's property. The node may be a `(Chain|Member)Expression` node. */ function isTargetMethod(node) { - return ( - node.type === "MemberExpression" && - TARGET_METHODS.test(astUtils.getStaticPropertyName(node) || "") - ); + return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS); +} + +/** + * Returns a human-legible description of an array method + * @param {string} arrayMethodName A method name to fully qualify + * @returns {string} the method name prefixed with `Array.` if it is a class method, + * or else `Array.prototype.` if it is an instance method. + */ +function fullMethodName(arrayMethodName) { + if (["from", "of", "isArray"].includes(arrayMethodName)) { + return "Array.".concat(arrayMethodName); + } + return "Array.prototype.".concat(arrayMethodName); } /** @@ -65,6 +73,7 @@ function getArrayMethodName(node) { */ case "LogicalExpression": case "ConditionalExpression": + case "ChainExpression": currentNode = parent; break; @@ -153,10 +162,10 @@ module.exports = { ], messages: { - expectedAtEnd: "Expected to return a value at the end of {{name}}.", - expectedInside: "Expected to return a value in {{name}}.", - expectedReturnValue: "{{name}} expected a return value.", - expectedNoReturnValue: "{{name}} did not expect a return value." + expectedAtEnd: "{{arrayMethodName}}() expects a value to be returned at the end of {{name}}.", + expectedInside: "{{arrayMethodName}}() expects a return value from {{name}}.", + expectedReturnValue: "{{arrayMethodName}}() expects a return value from {{name}}.", + expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}." } }, @@ -202,14 +211,13 @@ module.exports = { } if (messageId) { - let name = astUtils.getFunctionNameWithKind(node); + const name = astUtils.getFunctionNameWithKind(node); - name = messageId === "expectedNoReturnValue" ? lodash.upperFirst(name) : name; context.report({ node, loc: astUtils.getFunctionHeadLoc(node, sourceCode), messageId, - data: { name } + data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) } }); } } @@ -273,7 +281,8 @@ module.exports = { node, messageId, data: { - name: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node)) + name: astUtils.getFunctionNameWithKind(funcInfo.node), + arrayMethodName: fullMethodName(funcInfo.arrayMethodName) } }); } diff --git a/lib/rules/arrow-body-style.js b/lib/rules/arrow-body-style.js index 9d5c77d8573..7b318ea8b3a 100644 --- a/lib/rules/arrow-body-style.js +++ b/lib/rules/arrow-body-style.js @@ -75,6 +75,7 @@ module.exports = { const never = options[0] === "never"; const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral; const sourceCode = context.getSourceCode(); + let funcInfo = null; /** * Checks whether the given node has ASI problem or not. @@ -99,6 +100,21 @@ module.exports = { return sourceCode.getTokenAfter(node); } + /** + * Check whether the node is inside of a for loop's init + * @param {ASTNode} node node is inside for loop + * @returns {boolean} `true` if the node is inside of a for loop, else `false` + */ + function isInsideForLoopInitializer(node) { + if (node && node.parent) { + if (node.parent.type === "ForStatement" && node.parent.init === node) { + return true; + } + return isInsideForLoopInitializer(node.parent); + } + return false; + } + /** * Determines whether a arrow function body needs braces * @param {ASTNode} node The arrow function node. @@ -136,7 +152,7 @@ module.exports = { context.report({ node, - loc: arrowBody.loc.start, + loc: arrowBody.loc, messageId, fix(fixer) { const fixes = []; @@ -178,11 +194,13 @@ module.exports = { * If the first token of the reutrn value is `{` or the return value is a sequence expression, * enclose the return value by parentheses to avoid syntax error. */ - if (astUtils.isOpeningBraceToken(firstValueToken) || blockBody[0].argument.type === "SequenceExpression") { - fixes.push( - fixer.insertTextBefore(firstValueToken, "("), - fixer.insertTextAfter(lastValueToken, ")") - ); + if (astUtils.isOpeningBraceToken(firstValueToken) || blockBody[0].argument.type === "SequenceExpression" || (funcInfo.hasInOperator && isInsideForLoopInitializer(node))) { + if (!astUtils.isParenthesised(sourceCode, blockBody[0].argument)) { + fixes.push( + fixer.insertTextBefore(firstValueToken, "("), + fixer.insertTextAfter(lastValueToken, ")") + ); + } } /* @@ -201,7 +219,7 @@ module.exports = { if (always || (asNeeded && requireReturnForObjectLiteral && arrowBody.type === "ObjectExpression")) { context.report({ node, - loc: arrowBody.loc.start, + loc: arrowBody.loc, messageId: "expectedBlock", fix(fixer) { const fixes = []; @@ -245,7 +263,24 @@ module.exports = { } return { - "ArrowFunctionExpression:exit": validate + "BinaryExpression[operator='in']"() { + let info = funcInfo; + + while (info) { + info.hasInOperator = true; + info = info.upper; + } + }, + ArrowFunctionExpression() { + funcInfo = { + upper: funcInfo, + hasInOperator: false + }; + }, + "ArrowFunctionExpression:exit"(node) { + validate(node); + funcInfo = funcInfo.upper; + } }; } }; diff --git a/lib/rules/arrow-parens.js b/lib/rules/arrow-parens.js index bfd32447ac6..eaa1aab0238 100644 --- a/lib/rules/arrow-parens.js +++ b/lib/rules/arrow-parens.js @@ -15,15 +15,12 @@ const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ /** - * Get location should be reported by AST node. - * @param {ASTNode} node AST Node. - * @returns {Location} Location information. + * Determines if the given arrow function has block body. + * @param {ASTNode} node `ArrowFunctionExpression` node. + * @returns {boolean} `true` if the function has block body. */ -function getLocation(node) { - return { - start: node.params[0].loc.start, - end: node.params[node.params.length - 1].loc.end - }; +function hasBlockBody(node) { + return node.body.type === "BlockStatement"; } //------------------------------------------------------------------------------ @@ -75,126 +72,112 @@ module.exports = { const sourceCode = context.getSourceCode(); /** - * Determines whether a arrow function argument end with `)` - * @param {ASTNode} node The arrow function node. - * @returns {void} + * Finds opening paren of parameters for the given arrow function, if it exists. + * It is assumed that the given arrow function has exactly one parameter. + * @param {ASTNode} node `ArrowFunctionExpression` node. + * @returns {Token|null} the opening paren, or `null` if the given arrow function doesn't have parens of parameters. */ - function parens(node) { - const isAsync = node.async; - const firstTokenOfParam = sourceCode.getFirstToken(node, isAsync ? 1 : 0); - - /** - * Remove the parenthesis around a parameter - * @param {Fixer} fixer Fixer - * @returns {string} fixed parameter - */ - function fixParamsWithParenthesis(fixer) { - const paramToken = sourceCode.getTokenAfter(firstTokenOfParam); - - /* - * ES8 allows Trailing commas in function parameter lists and calls - * https://github.com/eslint/eslint/issues/8834 - */ - const closingParenToken = sourceCode.getTokenAfter(paramToken, astUtils.isClosingParenToken); - const asyncToken = isAsync ? sourceCode.getTokenBefore(firstTokenOfParam) : null; - const shouldAddSpaceForAsync = asyncToken && (asyncToken.range[1] === firstTokenOfParam.range[0]); - - return fixer.replaceTextRange([ - firstTokenOfParam.range[0], - closingParenToken.range[1] - ], `${shouldAddSpaceForAsync ? " " : ""}${paramToken.value}`); + function findOpeningParenOfParams(node) { + const tokenBeforeParams = sourceCode.getTokenBefore(node.params[0]); + + if ( + tokenBeforeParams && + astUtils.isOpeningParenToken(tokenBeforeParams) && + node.range[0] <= tokenBeforeParams.range[0] + ) { + return tokenBeforeParams; } - /** - * Checks whether there are comments inside the params or not. - * @returns {boolean} `true` if there are comments inside of parens, else `false` - */ - function hasCommentsInParens() { - if (astUtils.isOpeningParenToken(firstTokenOfParam)) { - const closingParenToken = sourceCode.getTokenAfter(node.params[0], astUtils.isClosingParenToken); + return null; + } - return closingParenToken && sourceCode.commentsExistBetween(firstTokenOfParam, closingParenToken); - } - return false; + /** + * Finds closing paren of parameters for the given arrow function. + * It is assumed that the given arrow function has parens of parameters and that it has exactly one parameter. + * @param {ASTNode} node `ArrowFunctionExpression` node. + * @returns {Token} the closing paren of parameters. + */ + function getClosingParenOfParams(node) { + return sourceCode.getTokenAfter(node.params[0], astUtils.isClosingParenToken); + } - } + /** + * Determines whether the given arrow function has comments inside parens of parameters. + * It is assumed that the given arrow function has parens of parameters. + * @param {ASTNode} node `ArrowFunctionExpression` node. + * @param {Token} openingParen Opening paren of parameters. + * @returns {boolean} `true` if the function has at least one comment inside of parens of parameters. + */ + function hasCommentsInParensOfParams(node, openingParen) { + return sourceCode.commentsExistBetween(openingParen, getClosingParenOfParams(node)); + } - if (hasCommentsInParens()) { - return; - } + /** + * Determines whether the given arrow function has unexpected tokens before opening paren of parameters, + * in which case it will be assumed that the existing parens of parameters are necessary. + * Only tokens within the range of the arrow function (tokens that are part of the arrow function) are taken into account. + * Example: (a) => b + * @param {ASTNode} node `ArrowFunctionExpression` node. + * @param {Token} openingParen Opening paren of parameters. + * @returns {boolean} `true` if the function has at least one unexpected token. + */ + function hasUnexpectedTokensBeforeOpeningParen(node, openingParen) { + const expectedCount = node.async ? 1 : 0; - // "as-needed", { "requireForBlockBody": true }: x => x - if ( - requireForBlockBody && - node.params[0].type === "Identifier" && - !node.params[0].typeAnnotation && - node.body.type !== "BlockStatement" && - !node.returnType - ) { - if (astUtils.isOpeningParenToken(firstTokenOfParam)) { - context.report({ - node, - messageId: "unexpectedParensInline", - loc: getLocation(node), - fix: fixParamsWithParenthesis - }); - } - return; - } + return sourceCode.getFirstToken(node, { skip: expectedCount }) !== openingParen; + } - if ( - requireForBlockBody && - node.body.type === "BlockStatement" - ) { - if (!astUtils.isOpeningParenToken(firstTokenOfParam)) { - context.report({ - node, - messageId: "expectedParensBlock", - loc: getLocation(node), - fix(fixer) { - return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`); - } - }); - } - return; - } + return { + "ArrowFunctionExpression[params.length=1]"(node) { + const shouldHaveParens = !asNeeded || requireForBlockBody && hasBlockBody(node); + const openingParen = findOpeningParenOfParams(node); + const hasParens = openingParen !== null; + const [param] = node.params; - // "as-needed": x => x - if (asNeeded && - node.params[0].type === "Identifier" && - !node.params[0].typeAnnotation && - !node.returnType - ) { - if (astUtils.isOpeningParenToken(firstTokenOfParam)) { + if (shouldHaveParens && !hasParens) { context.report({ node, - messageId: "unexpectedParens", - loc: getLocation(node), - fix: fixParamsWithParenthesis + messageId: requireForBlockBody ? "expectedParensBlock" : "expectedParens", + loc: param.loc, + *fix(fixer) { + yield fixer.insertTextBefore(param, "("); + yield fixer.insertTextAfter(param, ")"); + } }); } - return; - } - if (firstTokenOfParam.type === "Identifier") { - const after = sourceCode.getTokenAfter(firstTokenOfParam); - - // (x) => x - if (after.value !== ")") { + if ( + !shouldHaveParens && + hasParens && + param.type === "Identifier" && + !param.typeAnnotation && + !node.returnType && + !hasCommentsInParensOfParams(node, openingParen) && + !hasUnexpectedTokensBeforeOpeningParen(node, openingParen) + ) { context.report({ node, - messageId: "expectedParens", - loc: getLocation(node), - fix(fixer) { - return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`); + messageId: requireForBlockBody ? "unexpectedParensInline" : "unexpectedParens", + loc: param.loc, + *fix(fixer) { + const tokenBeforeOpeningParen = sourceCode.getTokenBefore(openingParen); + const closingParen = getClosingParenOfParams(node); + + if ( + tokenBeforeOpeningParen && + tokenBeforeOpeningParen.range[1] === openingParen.range[0] && + !astUtils.canTokensBeAdjacent(tokenBeforeOpeningParen, sourceCode.getFirstToken(param)) + ) { + yield fixer.insertTextBefore(openingParen, " "); + } + + // remove parens, whitespace inside parens, and possible trailing comma + yield fixer.removeRange([openingParen.range[0], param.range[0]]); + yield fixer.removeRange([param.range[1], closingParen.range[1]]); } }); } } - } - - return { - "ArrowFunctionExpression[params.length=1]": parens }; } }; diff --git a/lib/rules/camelcase.js b/lib/rules/camelcase.js index 04360837294..d34656cfabe 100644 --- a/lib/rules/camelcase.js +++ b/lib/rules/camelcase.js @@ -32,6 +32,10 @@ module.exports = { type: "boolean", default: false }, + ignoreGlobals: { + type: "boolean", + default: false + }, properties: { enum: ["always", "never"] }, @@ -61,8 +65,11 @@ module.exports = { let properties = options.properties || ""; const ignoreDestructuring = options.ignoreDestructuring; const ignoreImports = options.ignoreImports; + const ignoreGlobals = options.ignoreGlobals; const allow = options.allow || []; + let globalScope; + if (properties !== "always" && properties !== "never") { properties = "always"; } @@ -159,6 +166,37 @@ module.exports = { return false; } + /** + * Checks whether the given node represents a reference to a global variable that is not declared in the source code. + * These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables. + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a reference to a global variable. + */ + function isReferenceToGlobalVariable(node) { + const variable = globalScope.set.get(node.name); + + return variable && variable.defs.length === 0 && + variable.references.some(ref => ref.identifier === node); + } + + /** + * Checks whether the given node represents a reference to a property of an object in an object literal expression. + * This allows to differentiate between a global variable that is allowed to be used as a reference, and the key + * of the expressed object (which shouldn't be allowed). + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a property name of an object literal expression + */ + function isPropertyNameInObjectLiteral(node) { + const parent = node.parent; + + return ( + parent.type === "Property" && + parent.parent.type === "ObjectExpression" && + !parent.computed && + parent.key === node + ); + } + /** * Reports an AST node as a rule violation. * @param {ASTNode} node The node to report. @@ -174,6 +212,10 @@ module.exports = { return { + Program() { + globalScope = context.getScope(); + }, + Identifier(node) { /* @@ -189,6 +231,11 @@ module.exports = { return; } + // Check if it's a global variable + if (ignoreGlobals && isReferenceToGlobalVariable(node) && !isPropertyNameInObjectLiteral(node)) { + return; + } + // MemberExpressions get special rules if (node.parent.type === "MemberExpression") { diff --git a/lib/rules/consistent-return.js b/lib/rules/consistent-return.js index 22667fa4707..94db253d25b 100644 --- a/lib/rules/consistent-return.js +++ b/lib/rules/consistent-return.js @@ -9,23 +9,12 @@ //------------------------------------------------------------------------------ const lodash = require("lodash"); - const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ -/** - * Checks whether or not a given node is an `Identifier` node which was named a given name. - * @param {ASTNode} node A node to check. - * @param {string} name An expected name of the node. - * @returns {boolean} `true` if the node is an `Identifier` node which was named as expected. - */ -function isIdentifier(node, name) { - return node.type === "Identifier" && node.name === name; -} - /** * Checks whether or not a given code path segment is unreachable. * @param {CodePathSegment} segment A CodePathSegment to check. @@ -165,7 +154,7 @@ module.exports = { let hasReturnValue = Boolean(argument); if (treatUndefinedAsUnspecified && hasReturnValue) { - hasReturnValue = !isIdentifier(argument, "undefined") && argument.operator !== "void"; + hasReturnValue = !astUtils.isSpecificId(argument, "undefined") && argument.operator !== "void"; } if (!funcInfo.hasReturn) { diff --git a/lib/rules/constructor-super.js b/lib/rules/constructor-super.js index 5a848f210ca..65ed7422c25 100644 --- a/lib/rules/constructor-super.js +++ b/lib/rules/constructor-super.js @@ -50,6 +50,7 @@ function isPossibleConstructor(node) { case "MemberExpression": case "CallExpression": case "NewExpression": + case "ChainExpression": case "YieldExpression": case "TaggedTemplateExpression": case "MetaProperty": diff --git a/lib/rules/dot-location.js b/lib/rules/dot-location.js index d483e217a94..0a739b1712b 100644 --- a/lib/rules/dot-location.js +++ b/lib/rules/dot-location.js @@ -52,31 +52,37 @@ module.exports = { */ function checkDotLocation(node) { const property = node.property; - const dot = sourceCode.getTokenBefore(property); - - // `obj` expression can be parenthesized, but those paren tokens are not a part of the `obj` node. - const tokenBeforeDot = sourceCode.getTokenBefore(dot); - - const textBeforeDot = sourceCode.getText().slice(tokenBeforeDot.range[1], dot.range[0]); - const textAfterDot = sourceCode.getText().slice(dot.range[1], property.range[0]); + const dotToken = sourceCode.getTokenBefore(property); if (onObject) { - if (!astUtils.isTokenOnSameLine(tokenBeforeDot, dot)) { - const neededTextAfterToken = astUtils.isDecimalIntegerNumericToken(tokenBeforeDot) ? " " : ""; + // `obj` expression can be parenthesized, but those paren tokens are not a part of the `obj` node. + const tokenBeforeDot = sourceCode.getTokenBefore(dotToken); + + if (!astUtils.isTokenOnSameLine(tokenBeforeDot, dotToken)) { context.report({ node, - loc: dot.loc, + loc: dotToken.loc, messageId: "expectedDotAfterObject", - fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], property.range[0]], `${neededTextAfterToken}.${textBeforeDot}${textAfterDot}`) + *fix(fixer) { + if (dotToken.value.startsWith(".") && astUtils.isDecimalIntegerNumericToken(tokenBeforeDot)) { + yield fixer.insertTextAfter(tokenBeforeDot, ` ${dotToken.value}`); + } else { + yield fixer.insertTextAfter(tokenBeforeDot, dotToken.value); + } + yield fixer.remove(dotToken); + } }); } - } else if (!astUtils.isTokenOnSameLine(dot, property)) { + } else if (!astUtils.isTokenOnSameLine(dotToken, property)) { context.report({ node, - loc: dot.loc, + loc: dotToken.loc, messageId: "expectedDotBeforeProperty", - fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], property.range[0]], `${textBeforeDot}${textAfterDot}.`) + *fix(fixer) { + yield fixer.remove(dotToken); + yield fixer.insertTextBefore(property, dotToken.value); + } }); } } diff --git a/lib/rules/dot-notation.js b/lib/rules/dot-notation.js index 2e8fff8b90e..751b4628edc 100644 --- a/lib/rules/dot-notation.js +++ b/lib/rules/dot-notation.js @@ -87,28 +87,36 @@ module.exports = { data: { key: formattedValue }, - fix(fixer) { + *fix(fixer) { const leftBracket = sourceCode.getTokenAfter(node.object, astUtils.isOpeningBracketToken); const rightBracket = sourceCode.getLastToken(node); + const nextToken = sourceCode.getTokenAfter(node); - if (sourceCode.getFirstTokenBetween(leftBracket, rightBracket, { includeComments: true, filter: astUtils.isCommentToken })) { - - // Don't perform any fixes if there are comments inside the brackets. - return null; + // Don't perform any fixes if there are comments inside the brackets. + if (sourceCode.commentsExistBetween(leftBracket, rightBracket)) { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive } - const tokenAfterProperty = sourceCode.getTokenAfter(rightBracket); - const needsSpaceAfterProperty = tokenAfterProperty && - rightBracket.range[1] === tokenAfterProperty.range[0] && - !astUtils.canTokensBeAdjacent(String(value), tokenAfterProperty); - - const textBeforeDot = astUtils.isDecimalInteger(node.object) ? " " : ""; - const textAfterProperty = needsSpaceAfterProperty ? " " : ""; - - return fixer.replaceTextRange( + // Replace the brackets by an identifier. + if (!node.optional) { + yield fixer.insertTextBefore( + leftBracket, + astUtils.isDecimalInteger(node.object) ? " ." : "." + ); + } + yield fixer.replaceTextRange( [leftBracket.range[0], rightBracket.range[1]], - `${textBeforeDot}.${value}${textAfterProperty}` + value ); + + // Insert a space after the property if it will be connected to the next token. + if ( + nextToken && + rightBracket.range[1] === nextToken.range[0] && + !astUtils.canTokensBeAdjacent(String(value), nextToken) + ) { + yield fixer.insertTextAfter(node, " "); + } } }); } @@ -141,29 +149,24 @@ module.exports = { data: { key: node.property.name }, - fix(fixer) { - const dot = sourceCode.getTokenBefore(node.property); - const textAfterDot = sourceCode.text.slice(dot.range[1], node.property.range[0]); - - if (textAfterDot.trim()) { + *fix(fixer) { + const dotToken = sourceCode.getTokenBefore(node.property); - // Don't perform any fixes if there are comments between the dot and the property name. - return null; + // A statement that starts with `let[` is parsed as a destructuring variable declaration, not a MemberExpression. + if (node.object.type === "Identifier" && node.object.name === "let" && !node.optional) { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive } - if (node.object.type === "Identifier" && node.object.name === "let") { - - /* - * A statement that starts with `let[` is parsed as a destructuring variable declaration, not - * a MemberExpression. - */ - return null; + // Don't perform any fixes if there are comments between the dot and the property name. + if (sourceCode.commentsExistBetween(dotToken, node.property)) { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive } - return fixer.replaceTextRange( - [dot.range[0], node.property.range[1]], - `[${textAfterDot}"${node.property.name}"]` - ); + // Replace the identifier to brackets. + if (!node.optional) { + yield fixer.remove(dotToken); + } + yield fixer.replaceText(node.property, `["${node.property.name}"]`); } }); } diff --git a/lib/rules/func-call-spacing.js b/lib/rules/func-call-spacing.js index 5ecb63ecfa7..8fe690d4a6b 100644 --- a/lib/rules/func-call-spacing.js +++ b/lib/rules/func-call-spacing.js @@ -126,15 +126,24 @@ module.exports = { messageId: "unexpectedWhitespace", fix(fixer) { + // Don't remove comments. + if (sourceCode.commentsExistBetween(leftToken, rightToken)) { + return null; + } + + // If `?.` exsits, it doesn't hide no-undexpected-multiline errors + if (node.optional) { + return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], "?."); + } + /* * Only autofix if there is no newline * https://github.com/eslint/eslint/issues/7787 */ - if (!hasNewline) { - return fixer.removeRange([leftToken.range[1], rightToken.range[0]]); + if (hasNewline) { + return null; } - - return null; + return fixer.removeRange([leftToken.range[1], rightToken.range[0]]); } }); } else if (!never && !hasWhitespace) { @@ -149,6 +158,9 @@ module.exports = { }, messageId: "missing", fix(fixer) { + if (node.optional) { + return null; // Not sure if inserting a space to either before/after `?.` token. + } return fixer.insertTextBefore(rightToken, " "); } }); @@ -161,7 +173,31 @@ module.exports = { }, messageId: "unexpectedNewline", fix(fixer) { - return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], " "); + + /* + * Only autofix if there is no newline + * https://github.com/eslint/eslint/issues/7787 + * But if `?.` exsits, it doesn't hide no-undexpected-multiline errors + */ + if (!node.optional) { + return null; + } + + // Don't remove comments. + if (sourceCode.commentsExistBetween(leftToken, rightToken)) { + return null; + } + + const range = [leftToken.range[1], rightToken.range[0]]; + const qdToken = sourceCode.getTokenAfter(leftToken); + + if (qdToken.range[0] === leftToken.range[1]) { + return fixer.replaceTextRange(range, "?. "); + } + if (qdToken.range[1] === rightToken.range[0]) { + return fixer.replaceTextRange(range, " ?."); + } + return fixer.replaceTextRange(range, " ?. "); } }); } @@ -172,7 +208,7 @@ module.exports = { const lastToken = sourceCode.getLastToken(node); const lastCalleeToken = sourceCode.getLastToken(node.callee); const parenToken = sourceCode.getFirstTokenBetween(lastCalleeToken, lastToken, astUtils.isOpeningParenToken); - const prevToken = parenToken && sourceCode.getTokenBefore(parenToken); + const prevToken = parenToken && sourceCode.getTokenBefore(parenToken, astUtils.isNotQuestionDotToken); // Parens in NewExpression are optional if (!(parenToken && parenToken.range[1] < node.range[1])) { diff --git a/lib/rules/func-name-matching.js b/lib/rules/func-name-matching.js index 83430ffadfc..755c2ee5075 100644 --- a/lib/rules/func-name-matching.js +++ b/lib/rules/func-name-matching.js @@ -117,10 +117,7 @@ module.exports = { if (!node) { return false; } - return node.type === "CallExpression" && - node.callee.type === "MemberExpression" && - node.callee.object.name === objName && - node.callee.property.name === funcName; + return node.type === "CallExpression" && astUtils.isSpecificMemberAccess(node.callee, objName, funcName); } /** diff --git a/lib/rules/global-require.js b/lib/rules/global-require.js index 469c0175d25..09d0332007e 100644 --- a/lib/rules/global-require.js +++ b/lib/rules/global-require.js @@ -13,7 +13,8 @@ const ACCEPTABLE_PARENTS = [ "CallExpression", "ConditionalExpression", "Program", - "VariableDeclaration" + "VariableDeclaration", + "ChainExpression" ]; /** diff --git a/lib/rules/id-blacklist.js b/lib/rules/id-blacklist.js index d77a35d41b6..4fbba909fde 100644 --- a/lib/rules/id-blacklist.js +++ b/lib/rules/id-blacklist.js @@ -1,6 +1,6 @@ /** * @fileoverview Rule that warns when identifier names that are - * blacklisted in the configuration are used. + * specified in the configuration are used. * @author Keith Cirkel (http://keithcirkel.co.uk) */ @@ -111,6 +111,9 @@ function isShorthandPropertyDefinition(node) { module.exports = { meta: { + deprecated: true, + replacedBy: ["id-denylist"], + type: "suggestion", docs: { @@ -128,25 +131,25 @@ module.exports = { uniqueItems: true }, messages: { - blacklisted: "Identifier '{{name}}' is blacklisted." + restricted: "Identifier '{{name}}' is restricted." } }, create(context) { - const blacklist = new Set(context.options); + const denyList = new Set(context.options); const reportedNodes = new Set(); let globalScope; /** - * Checks whether the given name is blacklisted. + * Checks whether the given name is restricted. * @param {string} name The name to check. - * @returns {boolean} `true` if the name is blacklisted. + * @returns {boolean} `true` if the name is restricted. * @private */ - function isBlacklisted(name) { - return blacklist.has(name); + function isRestricted(name) { + return denyList.has(name); } /** @@ -172,8 +175,8 @@ module.exports = { /* * Member access has special rules for checking property names. - * Read access to a property with a blacklisted name is allowed, because it can be on an object that user has no control over. - * Write access isn't allowed, because it potentially creates a new property with a blacklisted name. + * Read access to a property with a restricted name is allowed, because it can be on an object that user has no control over. + * Write access isn't allowed, because it potentially creates a new property with a restricted name. */ if ( parent.type === "MemberExpression" && @@ -205,7 +208,7 @@ module.exports = { if (!reportedNodes.has(node)) { context.report({ node, - messageId: "blacklisted", + messageId: "restricted", data: { name: node.name } @@ -221,7 +224,7 @@ module.exports = { }, Identifier(node) { - if (isBlacklisted(node.name) && shouldCheck(node)) { + if (isRestricted(node.name) && shouldCheck(node)) { report(node); } } diff --git a/lib/rules/id-denylist.js b/lib/rules/id-denylist.js new file mode 100644 index 00000000000..112fd8a9d55 --- /dev/null +++ b/lib/rules/id-denylist.js @@ -0,0 +1,230 @@ +/** + * @fileoverview Rule that warns when identifier names that are + * specified in the configuration are used. + * @author Keith Cirkel (http://keithcirkel.co.uk) + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether the given node represents assignment target in a normal assignment or destructuring. + * @param {ASTNode} node The node to check. + * @returns {boolean} `true` if the node is assignment target. + */ +function isAssignmentTarget(node) { + const parent = node.parent; + + return ( + + // normal assignment + ( + parent.type === "AssignmentExpression" && + parent.left === node + ) || + + // destructuring + parent.type === "ArrayPattern" || + parent.type === "RestElement" || + ( + parent.type === "Property" && + parent.value === node && + parent.parent.type === "ObjectPattern" + ) || + ( + parent.type === "AssignmentPattern" && + parent.left === node + ) + ); +} + +/** + * Checks whether the given node represents an imported name that is renamed in the same import/export specifier. + * + * Examples: + * import { a as b } from 'mod'; // node `a` is renamed import + * export { a as b } from 'mod'; // node `a` is renamed import + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a renamed import. + */ +function isRenamedImport(node) { + const parent = node.parent; + + return ( + ( + parent.type === "ImportSpecifier" && + parent.imported !== parent.local && + parent.imported === node + ) || + ( + parent.type === "ExportSpecifier" && + parent.parent.source && // re-export + parent.local !== parent.exported && + parent.local === node + ) + ); +} + +/** + * Checks whether the given node is a renamed identifier node in an ObjectPattern destructuring. + * + * Examples: + * const { a : b } = foo; // node `a` is renamed node. + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a renamed node in an ObjectPattern destructuring. + */ +function isRenamedInDestructuring(node) { + const parent = node.parent; + + return ( + ( + !parent.computed && + parent.type === "Property" && + parent.parent.type === "ObjectPattern" && + parent.value !== node && + parent.key === node + ) + ); +} + +/** + * Checks whether the given node represents shorthand definition of a property in an object literal. + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a shorthand property definition. + */ +function isShorthandPropertyDefinition(node) { + const parent = node.parent; + + return ( + parent.type === "Property" && + parent.parent.type === "ObjectExpression" && + parent.shorthand + ); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: "suggestion", + + docs: { + description: "disallow specified identifiers", + category: "Stylistic Issues", + recommended: false, + url: "https://eslint.org/docs/rules/id-denylist" + }, + + schema: { + type: "array", + items: { + type: "string" + }, + uniqueItems: true + }, + messages: { + restricted: "Identifier '{{name}}' is restricted." + } + }, + + create(context) { + + const denyList = new Set(context.options); + const reportedNodes = new Set(); + + let globalScope; + + /** + * Checks whether the given name is restricted. + * @param {string} name The name to check. + * @returns {boolean} `true` if the name is restricted. + * @private + */ + function isRestricted(name) { + return denyList.has(name); + } + + /** + * Checks whether the given node represents a reference to a global variable that is not declared in the source code. + * These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables. + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a reference to a global variable. + */ + function isReferenceToGlobalVariable(node) { + const variable = globalScope.set.get(node.name); + + return variable && variable.defs.length === 0 && + variable.references.some(ref => ref.identifier === node); + } + + /** + * Determines whether the given node should be checked. + * @param {ASTNode} node `Identifier` node. + * @returns {boolean} `true` if the node should be checked. + */ + function shouldCheck(node) { + const parent = node.parent; + + /* + * Member access has special rules for checking property names. + * Read access to a property with a restricted name is allowed, because it can be on an object that user has no control over. + * Write access isn't allowed, because it potentially creates a new property with a restricted name. + */ + if ( + parent.type === "MemberExpression" && + parent.property === node && + !parent.computed + ) { + return isAssignmentTarget(parent); + } + + return ( + parent.type !== "CallExpression" && + parent.type !== "NewExpression" && + !isRenamedImport(node) && + !isRenamedInDestructuring(node) && + !( + isReferenceToGlobalVariable(node) && + !isShorthandPropertyDefinition(node) + ) + ); + } + + /** + * Reports an AST node as a rule violation. + * @param {ASTNode} node The node to report. + * @returns {void} + * @private + */ + function report(node) { + if (!reportedNodes.has(node)) { + context.report({ + node, + messageId: "restricted", + data: { + name: node.name + } + }); + reportedNodes.add(node); + } + } + + return { + + Program() { + globalScope = context.getScope(); + }, + + Identifier(node) { + if (isRestricted(node.name) && shouldCheck(node)) { + report(node); + } + } + }; + } +}; diff --git a/lib/rules/indent.js b/lib/rules/indent.js index d576fde0382..22b633845b5 100644 --- a/lib/rules/indent.js +++ b/lib/rules/indent.js @@ -32,6 +32,7 @@ const KNOWN_NODES = new Set([ "BreakStatement", "CallExpression", "CatchClause", + "ChainExpression", "ClassBody", "ClassDeclaration", "ClassExpression", @@ -934,6 +935,24 @@ module.exports = { parameterParens.add(openingParen); parameterParens.add(closingParen); + /* + * If `?.` token exists, set desired offset for that. + * This logic is copied from `MemberExpression`'s. + */ + if (node.optional) { + const dotToken = sourceCode.getTokenAfter(node.callee, astUtils.isQuestionDotToken); + const calleeParenCount = sourceCode.getTokensBetween(node.callee, dotToken, { filter: astUtils.isClosingParenToken }).length; + const firstTokenOfCallee = calleeParenCount + ? sourceCode.getTokenBefore(node.callee, { skip: calleeParenCount - 1 }) + : sourceCode.getFirstToken(node.callee); + const lastTokenOfCallee = sourceCode.getTokenBefore(dotToken); + const offsetBase = lastTokenOfCallee.loc.end.line === openingParen.loc.start.line + ? lastTokenOfCallee + : firstTokenOfCallee; + + offsets.setDesiredOffset(dotToken, offsetBase, 1); + } + const offsetAfterToken = node.callee.type === "TaggedTemplateExpression" ? sourceCode.getFirstToken(node.callee.quasi) : openingParen; const offsetToken = sourceCode.getTokenBefore(offsetAfterToken); diff --git a/lib/rules/index.js b/lib/rules/index.js index 567cd4a0eca..3cf26e51bc8 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -57,6 +57,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({ "guard-for-in": () => require("./guard-for-in"), "handle-callback-err": () => require("./handle-callback-err"), "id-blacklist": () => require("./id-blacklist"), + "id-denylist": () => require("./id-denylist"), "id-length": () => require("./id-length"), "id-match": () => require("./id-match"), "implicit-arrow-linebreak": () => require("./implicit-arrow-linebreak"), diff --git a/lib/rules/keyword-spacing.js b/lib/rules/keyword-spacing.js index 99979a32a5b..913cf4682f9 100644 --- a/lib/rules/keyword-spacing.js +++ b/lib/rules/keyword-spacing.js @@ -126,7 +126,7 @@ module.exports = { !sourceCode.isSpaceBetweenTokens(prevToken, token) ) { context.report({ - loc: token.loc.start, + loc: token.loc, messageId: "expectedBefore", data: token, fix(fixer) { @@ -178,7 +178,7 @@ module.exports = { !sourceCode.isSpaceBetweenTokens(token, nextToken) ) { context.report({ - loc: token.loc.start, + loc: token.loc, messageId: "expectedAfter", data: token, fix(fixer) { diff --git a/lib/rules/max-len.js b/lib/rules/max-len.js index 995e0c52026..dd76760c505 100644 --- a/lib/rules/max-len.js +++ b/lib/rules/max-len.js @@ -383,11 +383,22 @@ module.exports = { return; } + const loc = { + start: { + line: lineNumber, + column: 0 + }, + end: { + line: lineNumber, + column: textToMeasure.length + } + }; + if (commentLengthApplies) { if (lineLength > maxCommentLength) { context.report({ node, - loc: { line: lineNumber, column: 0 }, + loc, messageId: "maxComment", data: { lineLength, @@ -398,7 +409,7 @@ module.exports = { } else if (lineLength > maxLength) { context.report({ node, - loc: { line: lineNumber, column: 0 }, + loc, messageId: "max", data: { lineLength, diff --git a/lib/rules/new-cap.js b/lib/rules/new-cap.js index 0faf45efb92..4249a542802 100644 --- a/lib/rules/new-cap.js +++ b/lib/rules/new-cap.js @@ -158,15 +158,9 @@ module.exports = { * @returns {string} name */ function extractNameFromExpression(node) { - - let name = ""; - - if (node.callee.type === "MemberExpression") { - name = astUtils.getStaticPropertyName(node.callee) || ""; - } else { - name = node.callee.name; - } - return name; + return node.callee.type === "Identifier" + ? node.callee.name + : astUtils.getStaticPropertyName(node.callee) || ""; } /** @@ -212,14 +206,16 @@ module.exports = { return true; } - if (calleeName === "UTC" && node.callee.type === "MemberExpression") { + const callee = astUtils.skipChainExpression(node.callee); + + if (calleeName === "UTC" && callee.type === "MemberExpression") { // allow if callee is Date.UTC - return node.callee.object.type === "Identifier" && - node.callee.object.name === "Date"; + return callee.object.type === "Identifier" && + callee.object.name === "Date"; } - return skipProperties && node.callee.type === "MemberExpression"; + return skipProperties && callee.type === "MemberExpression"; } /** @@ -229,7 +225,7 @@ module.exports = { * @returns {void} */ function report(node, messageId) { - let callee = node.callee; + let callee = astUtils.skipChainExpression(node.callee); if (callee.type === "MemberExpression") { callee = callee.property; diff --git a/lib/rules/newline-per-chained-call.js b/lib/rules/newline-per-chained-call.js index 4254fec185e..46c9d6c10f8 100644 --- a/lib/rules/newline-per-chained-call.js +++ b/lib/rules/newline-per-chained-call.js @@ -57,7 +57,16 @@ module.exports = { * @returns {string} The prefix of the node. */ function getPrefix(node) { - return node.computed ? "[" : "."; + if (node.computed) { + if (node.optional) { + return "?.["; + } + return "["; + } + if (node.optional) { + return "?."; + } + return "."; } /** @@ -76,17 +85,18 @@ module.exports = { return { "CallExpression:exit"(node) { - if (!node.callee || node.callee.type !== "MemberExpression") { + const callee = astUtils.skipChainExpression(node.callee); + + if (callee.type !== "MemberExpression") { return; } - const callee = node.callee; - let parent = callee.object; + let parent = astUtils.skipChainExpression(callee.object); let depth = 1; while (parent && parent.callee) { depth += 1; - parent = parent.callee.object; + parent = astUtils.skipChainExpression(astUtils.skipChainExpression(parent.callee).object); } if (depth > ignoreChainWithDepth && astUtils.isTokenOnSameLine(callee.object, callee.property)) { diff --git a/lib/rules/no-alert.js b/lib/rules/no-alert.js index 22d0dd57bdd..702b4d2ba7c 100644 --- a/lib/rules/no-alert.js +++ b/lib/rules/no-alert.js @@ -10,7 +10,8 @@ const { getStaticPropertyName: getPropertyName, - getVariableByName + getVariableByName, + skipChainExpression } = require("./utils/ast-utils"); //------------------------------------------------------------------------------ @@ -64,7 +65,13 @@ function isGlobalThisReferenceOrGlobalWindow(scope, node) { if (scope.type === "global" && node.type === "ThisExpression") { return true; } - if (node.name === "window" || (node.name === "globalThis" && getVariableByName(scope, "globalThis"))) { + if ( + node.type === "Identifier" && + ( + node.name === "window" || + (node.name === "globalThis" && getVariableByName(scope, "globalThis")) + ) + ) { return !isShadowed(scope, node); } @@ -96,7 +103,7 @@ module.exports = { create(context) { return { CallExpression(node) { - const callee = node.callee, + const callee = skipChainExpression(node.callee), currentScope = context.getScope(); // without window. diff --git a/lib/rules/no-duplicate-case.js b/lib/rules/no-duplicate-case.js index c8a0fa9da3c..e2d9665e7f5 100644 --- a/lib/rules/no-duplicate-case.js +++ b/lib/rules/no-duplicate-case.js @@ -6,6 +6,12 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -31,18 +37,31 @@ module.exports = { create(context) { const sourceCode = context.getSourceCode(); + /** + * Determines whether the two given nodes are considered to be equal. + * @param {ASTNode} a First node. + * @param {ASTNode} b Second node. + * @returns {boolean} `true` if the nodes are considered to be equal. + */ + function equal(a, b) { + if (a.type !== b.type) { + return false; + } + + return astUtils.equalTokens(a, b, sourceCode); + } return { SwitchStatement(node) { - const previousKeys = new Set(); + const previousTests = []; for (const switchCase of node.cases) { if (switchCase.test) { - const key = sourceCode.getText(switchCase.test); + const test = switchCase.test; - if (previousKeys.has(key)) { + if (previousTests.some(previousTest => equal(previousTest, test))) { context.report({ node: switchCase, messageId: "unexpected" }); } else { - previousKeys.add(key); + previousTests.push(test); } } } diff --git a/lib/rules/no-eval.js b/lib/rules/no-eval.js index 811ad4e5d73..a020fdee014 100644 --- a/lib/rules/no-eval.js +++ b/lib/rules/no-eval.js @@ -21,38 +21,6 @@ const candidatesOfGlobalObject = Object.freeze([ "globalThis" ]); -/** - * Checks a given node is a Identifier node of the specified name. - * @param {ASTNode} node A node to check. - * @param {string} name A name to check. - * @returns {boolean} `true` if the node is a Identifier node of the name. - */ -function isIdentifier(node, name) { - return node.type === "Identifier" && node.name === name; -} - -/** - * Checks a given node is a Literal node of the specified string value. - * @param {ASTNode} node A node to check. - * @param {string} name A name to check. - * @returns {boolean} `true` if the node is a Literal node of the name. - */ -function isConstant(node, name) { - switch (node.type) { - case "Literal": - return node.value === name; - - case "TemplateLiteral": - return ( - node.expressions.length === 0 && - node.quasis[0].value.cooked === name - ); - - default: - return false; - } -} - /** * Checks a given node is a MemberExpression node which has the specified name's * property. @@ -62,10 +30,7 @@ function isConstant(node, name) { * the specified name's property */ function isMember(node, name) { - return ( - node.type === "MemberExpression" && - (node.computed ? isConstant : isIdentifier)(node.property, name) - ); + return astUtils.isSpecificMemberAccess(node, null, name); } //------------------------------------------------------------------------------ @@ -230,7 +195,12 @@ module.exports = { "CallExpression:exit"(node) { const callee = node.callee; - if (isIdentifier(callee, "eval")) { + /* + * Optional call (`eval?.("code")`) is not direct eval. + * The direct eval is only step 6.a.vi of https://tc39.es/ecma262/#sec-function-calls-runtime-semantics-evaluation + * But the optional call is https://tc39.es/ecma262/#sec-optional-chaining-chain-evaluation + */ + if (!node.optional && astUtils.isSpecificId(callee, "eval")) { report(callee); } } @@ -241,7 +211,7 @@ module.exports = { "CallExpression:exit"(node) { const callee = node.callee; - if (isIdentifier(callee, "eval")) { + if (astUtils.isSpecificId(callee, "eval")) { report(callee); } }, diff --git a/lib/rules/no-extend-native.js b/lib/rules/no-extend-native.js index 7ab25ab4895..db365b50924 100644 --- a/lib/rules/no-extend-native.js +++ b/lib/rules/no-extend-native.js @@ -12,12 +12,6 @@ const astUtils = require("./utils/ast-utils"); const globals = require("globals"); -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -const propertyDefinitionMethods = new Set(["defineProperty", "defineProperties"]); - //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -100,40 +94,30 @@ module.exports = { } /** - * 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. + * Check if it's an assignment to the property of the given node. + * Example: `*.prop = 0` // the `*` is the given node. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if an assignment to the property of the node. */ - 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 + function isAssigningToPropertyOf(node) { + return ( + node.parent.type === "MemberExpression" && + node.parent.object === node && + node.parent.parent.type === "AssignmentExpression" && + node.parent.parent.left === node.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. + * Checks if the given node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`. */ - 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) + function isInDefinePropertyCall(node) { + return ( + node.parent.type === "CallExpression" && + node.parent.arguments[0] === node && + astUtils.isSpecificMemberAccess(node.parent.callee, "Object", /^definePropert(?:y|ies)$/u) ); } @@ -149,14 +133,27 @@ module.exports = { * @returns {void} */ function checkAndReportPrototypeExtension(identifierNode) { - if (isInPrototypePropertyAssignment(identifierNode)) { + if (!isPrototypePropertyAccessed(identifierNode)) { + return; // This is not `*.prototype` access. + } + + /* + * `identifierNode.parent` is a MamberExpression `*.prototype`. + * If it's an optional member access, it may be wrapped by a `ChainExpression` node. + */ + const prototypeNode = + identifierNode.parent.parent.type === "ChainExpression" + ? identifierNode.parent.parent + : identifierNode.parent; + + if (isAssigningToPropertyOf(prototypeNode)) { - // Identifier --> MemberExpression --> MemberExpression --> AssignmentExpression - reportNode(identifierNode.parent.parent.parent, identifierNode.name); - } else if (isInDefinePropertyCall(identifierNode)) { + // `*.prototype` -> MemberExpression -> AssignmentExpression + reportNode(prototypeNode.parent.parent, identifierNode.name); + } else if (isInDefinePropertyCall(prototypeNode)) { - // Identifier --> MemberExpression --> CallExpression - reportNode(identifierNode.parent.parent, identifierNode.name); + // `*.prototype` -> CallExpression + reportNode(prototypeNode.parent, identifierNode.name); } } diff --git a/lib/rules/no-extra-bind.js b/lib/rules/no-extra-bind.js index df695924ab5..2db440dc1ea 100644 --- a/lib/rules/no-extra-bind.js +++ b/lib/rules/no-extra-bind.js @@ -61,24 +61,62 @@ module.exports = { * @returns {void} */ function report(node) { + const memberNode = node.parent; + const callNode = memberNode.parent.type === "ChainExpression" + ? memberNode.parent.parent + : memberNode.parent; + context.report({ - node: node.parent.parent, + node: callNode, messageId: "unexpected", - loc: node.parent.property.loc, + loc: memberNode.property.loc, + fix(fixer) { - if (node.parent.parent.arguments.length && !isSideEffectFree(node.parent.parent.arguments[0])) { + if (!isSideEffectFree(callNode.arguments[0])) { return null; } - const firstTokenToRemove = sourceCode - .getFirstTokenBetween(node.parent.object, node.parent.property, astUtils.isNotClosingParenToken); - const lastTokenToRemove = sourceCode.getLastToken(node.parent.parent); + /* + * The list of the first/last token pair of a removal range. + * This is two parts because closing parentheses may exist between the method name and arguments. + * E.g. `(function(){}.bind ) (obj)` + * ^^^^^ ^^^^^ < removal ranges + * E.g. `(function(){}?.['bind'] ) ?.(obj)` + * ^^^^^^^^^^ ^^^^^^^ < removal ranges + */ + const tokenPairs = [ + [ + + // `.`, `?.`, or `[` token. + sourceCode.getTokenAfter( + memberNode.object, + astUtils.isNotClosingParenToken + ), + + // property name or `]` token. + sourceCode.getLastToken(memberNode) + ], + [ + + // `?.` or `(` token of arguments. + sourceCode.getTokenAfter( + memberNode, + astUtils.isNotClosingParenToken + ), + + // `)` token of arguments. + sourceCode.getLastToken(callNode) + ] + ]; + const firstTokenToRemove = tokenPairs[0][0]; + const lastTokenToRemove = tokenPairs[1][1]; if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) { return null; } - return fixer.removeRange([firstTokenToRemove.range[0], node.parent.parent.range[1]]); + return tokenPairs.map(([start, end]) => + fixer.removeRange([start.range[0], end.range[1]])); } }); } @@ -93,18 +131,20 @@ module.exports = { * @returns {boolean} `true` if the node is the callee of `.bind()` method. */ function isCalleeOfBindMethod(node) { - const parent = node.parent; - const grandparent = parent.parent; + if (!astUtils.isSpecificMemberAccess(node.parent, null, "bind")) { + return false; + } + + // The node of `*.bind` member access. + const bindNode = node.parent.parent.type === "ChainExpression" + ? node.parent.parent + : node.parent; return ( - grandparent && - grandparent.type === "CallExpression" && - grandparent.callee === parent && - grandparent.arguments.length === 1 && - grandparent.arguments[0].type !== "SpreadElement" && - parent.type === "MemberExpression" && - parent.object === node && - astUtils.getStaticPropertyName(parent) === "bind" + bindNode.parent.type === "CallExpression" && + bindNode.parent.callee === bindNode && + bindNode.parent.arguments.length === 1 && + bindNode.parent.arguments[0].type !== "SpreadElement" ); } diff --git a/lib/rules/no-extra-boolean-cast.js b/lib/rules/no-extra-boolean-cast.js index b90757b1126..6ae3ea62ca7 100644 --- a/lib/rules/no-extra-boolean-cast.js +++ b/lib/rules/no-extra-boolean-cast.js @@ -111,6 +111,10 @@ module.exports = { * @returns {boolean} If the node is in one of the flagged contexts */ function isInFlaggedContext(node) { + if (node.parent.type === "ChainExpression") { + return isInFlaggedContext(node.parent); + } + return isInBooleanContext(node) || (isLogicalContext(node.parent) && @@ -149,6 +153,9 @@ module.exports = { * @returns {boolean} `true` if the node needs to be parenthesized. */ function needsParens(previousNode, node) { + if (previousNode.parent.type === "ChainExpression") { + return needsParens(previousNode.parent, node); + } if (isParenthesized(previousNode)) { // parentheses around the previous node will stay, so there is no need for an additional pair diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js index bae1a498cf0..e9d394c616a 100644 --- a/lib/rules/no-extra-parens.js +++ b/lib/rules/no-extra-parens.js @@ -100,10 +100,18 @@ module.exports = { * @private */ function isImmediateFunctionPrototypeMethodCall(node) { - return node.type === "CallExpression" && - node.callee.type === "MemberExpression" && - node.callee.object.type === "FunctionExpression" && - ["call", "apply"].includes(astUtils.getStaticPropertyName(node.callee)); + const callNode = astUtils.skipChainExpression(node); + + if (callNode.type !== "CallExpression") { + return false; + } + const callee = astUtils.skipChainExpression(callNode.callee); + + return ( + callee.type === "MemberExpression" && + callee.object.type === "FunctionExpression" && + ["call", "apply"].includes(astUtils.getStaticPropertyName(callee)) + ); } /** @@ -360,7 +368,9 @@ module.exports = { * @returns {boolean} `true` if the given node is an IIFE */ function isIIFE(node) { - return node.type === "CallExpression" && node.callee.type === "FunctionExpression"; + const maybeCallNode = astUtils.skipChainExpression(node); + + return maybeCallNode.type === "CallExpression" && maybeCallNode.callee.type === "FunctionExpression"; } /** @@ -466,13 +476,16 @@ module.exports = { if ( hasDoubleExcessParens(callee) || - !isIIFE(node) && !hasNewParensException && !( + !isIIFE(node) && + !hasNewParensException && + !( // Allow extra parens around a new expression if they are intervening parentheses. node.type === "NewExpression" && callee.type === "MemberExpression" && doesMemberExpressionContainCallExpression(callee) - ) + ) && + !(!node.optional && callee.type === "ChainExpression") ) { report(node.callee); } @@ -710,6 +723,20 @@ module.exports = { reportsBuffer.reports = reportsBuffer.reports.filter(r => r.node !== node); } + /** + * Checks whether a node is a MemberExpression at NewExpression's callee. + * @param {ASTNode} node node to check. + * @returns {boolean} True if the node is a MemberExpression at NewExpression's callee. false otherwise. + */ + function isMemberExpInNewCallee(node) { + if (node.type === "MemberExpression") { + return node.parent.type === "NewExpression" && node.parent.callee === node + ? true + : node.parent.object === node && isMemberExpInNewCallee(node.parent); + } + return false; + } + return { ArrayExpression(node) { node.elements @@ -950,7 +977,11 @@ module.exports = { LogicalExpression: checkBinaryLogical, MemberExpression(node) { - const nodeObjHasExcessParens = hasExcessParens(node.object) && + const shouldAllowWrapOnce = isMemberExpInNewCallee(node) && + doesMemberExpressionContainCallExpression(node); + const nodeObjHasExcessParens = shouldAllowWrapOnce + ? hasDoubleExcessParens(node.object) + : hasExcessParens(node.object) && !( isImmediateFunctionPrototypeMethodCall(node.parent) && node.parent.callee === node && @@ -974,8 +1005,8 @@ module.exports = { } if (nodeObjHasExcessParens && - node.object.type === "CallExpression" && - node.parent.type !== "NewExpression") { + node.object.type === "CallExpression" + ) { report(node.object); } @@ -986,6 +1017,13 @@ module.exports = { report(node.object); } + if (nodeObjHasExcessParens && + node.optional && + node.object.type === "ChainExpression" + ) { + report(node.object); + } + if (node.computed && hasExcessParens(node.property)) { report(node.property); } diff --git a/lib/rules/no-implicit-coercion.js b/lib/rules/no-implicit-coercion.js index 6d5ee61e96b..a639711ecea 100644 --- a/lib/rules/no-implicit-coercion.js +++ b/lib/rules/no-implicit-coercion.js @@ -47,12 +47,14 @@ function isDoubleLogicalNegating(node) { * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling. */ function isBinaryNegatingOfIndexOf(node) { + if (node.operator !== "~") { + return false; + } + const callNode = astUtils.skipChainExpression(node.argument); + return ( - node.operator === "~" && - node.argument.type === "CallExpression" && - node.argument.callee.type === "MemberExpression" && - node.argument.callee.property.type === "Identifier" && - INDEX_OF_PATTERN.test(node.argument.callee.property.name) + callNode.type === "CallExpression" && + astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN) ); } @@ -246,7 +248,10 @@ module.exports = { // ~foo.indexOf(bar) operatorAllowed = options.allow.indexOf("~") >= 0; if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) { - const recommendation = `${sourceCode.getText(node.argument)} !== -1`; + + // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case. + const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1"; + const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`; report(node, recommendation, false); } diff --git a/lib/rules/no-implied-eval.js b/lib/rules/no-implied-eval.js index 1668a0432a5..b8120a64887 100644 --- a/lib/rules/no-implied-eval.js +++ b/lib/rules/no-implied-eval.js @@ -35,8 +35,8 @@ module.exports = { }, create(context) { - const EVAL_LIKE_FUNCS = Object.freeze(["setTimeout", "execScript", "setInterval"]); const GLOBAL_CANDIDATES = Object.freeze(["global", "window", "globalThis"]); + const EVAL_LIKE_FUNC_PATTERN = /^(?:set(?:Interval|Timeout)|execScript)$/u; /** * Checks whether a node is evaluated as a string or not. @@ -56,28 +56,6 @@ module.exports = { return false; } - /** - * Checks whether a node is an Identifier node named one of the specified names. - * @param {ASTNode} node A node to check. - * @param {string[]} specifiers Array of specified name. - * @returns {boolean} True if the node is a Identifier node which has specified name. - */ - function isSpecifiedIdentifier(node, specifiers) { - return node.type === "Identifier" && specifiers.includes(node.name); - } - - /** - * Checks a given node is a MemberExpression node which has the specified name's - * property. - * @param {ASTNode} node A node to check. - * @param {string[]} specifiers Array of specified name. - * @returns {boolean} `true` if the node is a MemberExpression node which has - * the specified name's property - */ - function isSpecifiedMember(node, specifiers) { - return node.type === "MemberExpression" && specifiers.includes(astUtils.getStaticPropertyName(node)); - } - /** * Reports if the `CallExpression` node has evaluated argument. * @param {ASTNode} node A CallExpression to check. @@ -114,14 +92,15 @@ module.exports = { const identifier = ref.identifier; let node = identifier.parent; - while (isSpecifiedMember(node, [name])) { + while (astUtils.isSpecificMemberAccess(node, null, name)) { node = node.parent; } - if (isSpecifiedMember(node, EVAL_LIKE_FUNCS)) { - const parent = node.parent; + if (astUtils.isSpecificMemberAccess(node, null, EVAL_LIKE_FUNC_PATTERN)) { + const calleeNode = node.parent.type === "ChainExpression" ? node.parent : node; + const parent = calleeNode.parent; - if (parent.type === "CallExpression" && parent.callee === node) { + if (parent.type === "CallExpression" && parent.callee === calleeNode) { reportImpliedEvalCallExpression(parent); } } @@ -134,7 +113,7 @@ module.exports = { return { CallExpression(node) { - if (isSpecifiedIdentifier(node.callee, EVAL_LIKE_FUNCS)) { + if (astUtils.isSpecificId(node.callee, EVAL_LIKE_FUNC_PATTERN)) { reportImpliedEvalCallExpression(node); } }, diff --git a/lib/rules/no-import-assign.js b/lib/rules/no-import-assign.js index 32e445ff68b..7a349bb730b 100644 --- a/lib/rules/no-import-assign.js +++ b/lib/rules/no-import-assign.js @@ -9,16 +9,12 @@ // Helpers //------------------------------------------------------------------------------ -const { findVariable, getPropertyName } = require("eslint-utils"); - -const MutationMethods = { - Object: new Set([ - "assign", "defineProperties", "defineProperty", "freeze", - "setPrototypeOf" - ]), - Reflect: new Set([ - "defineProperty", "deleteProperty", "set", "setPrototypeOf" - ]) +const { findVariable } = require("eslint-utils"); +const astUtils = require("./utils/ast-utils"); + +const WellKnownMutationFunctions = { + Object: /^(?:assign|definePropert(?:y|ies)|freeze|setPrototypeOf)$/u, + Reflect: /^(?:(?:define|delete)Property|set(?:PrototypeOf)?)$/u }; /** @@ -56,17 +52,20 @@ function isAssignmentLeft(node) { * @returns {boolean} `true` if the node is the operand of mutation unary operator. */ function isOperandOfMutationUnaryOperator(node) { - const { parent } = node; + const argumentNode = node.parent.type === "ChainExpression" + ? node.parent + : node; + const { parent } = argumentNode; return ( ( parent.type === "UpdateExpression" && - parent.argument === node + parent.argument === argumentNode ) || ( parent.type === "UnaryExpression" && parent.operator === "delete" && - parent.argument === node + parent.argument === argumentNode ) ); } @@ -92,35 +91,37 @@ function isIterationVariable(node) { } /** - * Check if a given node is the iteration variable of `for-in`/`for-of` syntax. + * Check if a given node is at the first argument of a well-known mutation function. + * - `Object.assign` + * - `Object.defineProperty` + * - `Object.defineProperties` + * - `Object.freeze` + * - `Object.setPrototypeOf` + * - `Refrect.defineProperty` + * - `Refrect.deleteProperty` + * - `Refrect.set` + * - `Refrect.setPrototypeOf` * @param {ASTNode} node The node to check. * @param {Scope} scope A `escope.Scope` object to find variable (whichever). - * @returns {boolean} `true` if the node is the iteration variable. + * @returns {boolean} `true` if the node is at the first argument of a well-known mutation function. */ function isArgumentOfWellKnownMutationFunction(node, scope) { const { parent } = node; + if (parent.type !== "CallExpression" || parent.arguments[0] !== node) { + return false; + } + const callee = astUtils.skipChainExpression(parent.callee); + if ( - parent.type === "CallExpression" && - parent.arguments[0] === node && - parent.callee.type === "MemberExpression" && - parent.callee.object.type === "Identifier" + !astUtils.isSpecificMemberAccess(callee, "Object", WellKnownMutationFunctions.Object) && + !astUtils.isSpecificMemberAccess(callee, "Reflect", WellKnownMutationFunctions.Reflect) ) { - const { callee } = parent; - const { object } = callee; - - if (Object.keys(MutationMethods).includes(object.name)) { - const variable = findVariable(scope, object); - - return ( - variable !== null && - variable.scope.type === "global" && - MutationMethods[object.name].has(getPropertyName(callee, scope)) - ); - } + return false; } + const variable = findVariable(scope, callee.object); - return false; + return variable !== null && variable.scope.type === "global"; } /** diff --git a/lib/rules/no-irregular-whitespace.js b/lib/rules/no-irregular-whitespace.js index 21842331f21..0bf69b128e6 100644 --- a/lib/rules/no-irregular-whitespace.js +++ b/lib/rules/no-irregular-whitespace.js @@ -91,7 +91,7 @@ module.exports = { const locStart = node.loc.start; const locEnd = node.loc.end; - errors = errors.filter(({ loc: errorLoc }) => { + errors = errors.filter(({ loc: { start: errorLoc } }) => { if (errorLoc.line >= locStart.line && errorLoc.line <= locEnd.line) { if (errorLoc.column >= locStart.column && (errorLoc.column <= locEnd.column || errorLoc.line < locEnd.line)) { return false; @@ -160,15 +160,19 @@ module.exports = { let match; while ((match = IRREGULAR_WHITESPACE.exec(sourceLine)) !== null) { - const location = { - line: lineNumber, - column: match.index - }; - errors.push({ node, messageId: "noIrregularWhitespace", - loc: location + loc: { + start: { + line: lineNumber, + column: match.index + }, + end: { + line: lineNumber, + column: match.index + match[0].length + } + } }); } }); @@ -189,16 +193,22 @@ module.exports = { while ((match = IRREGULAR_LINE_TERMINATORS.exec(source)) !== null) { const lineIndex = linebreaks.indexOf(match[0], lastLineIndex + 1) || 0; - const location = { - line: lineIndex + 1, - column: sourceLines[lineIndex].length - }; errors.push({ node, messageId: "noIrregularWhitespace", - loc: location + loc: { + start: { + line: lineIndex + 1, + column: sourceLines[lineIndex].length + }, + end: { + line: lineIndex + 2, + column: 0 + } + } }); + lastLineIndex = lineIndex; } } diff --git a/lib/rules/no-magic-numbers.js b/lib/rules/no-magic-numbers.js index 5d7b9728c61..510b3f9b261 100644 --- a/lib/rules/no-magic-numbers.js +++ b/lib/rules/no-magic-numbers.js @@ -5,7 +5,7 @@ "use strict"; -const { isNumericLiteral } = require("./utils/ast-utils"); +const astUtils = require("./utils/ast-utils"); // Maximum array length by the ECMAScript Specification. const MAX_ARRAY_LENGTH = 2 ** 32 - 1; @@ -116,12 +116,8 @@ module.exports = { return parent.type === "CallExpression" && fullNumberNode === parent.arguments[1] && ( - parent.callee.name === "parseInt" || - ( - parent.callee.type === "MemberExpression" && - parent.callee.object.name === "Number" && - parent.callee.property.name === "parseInt" - ) + astUtils.isSpecificId(parent.callee, "parseInt") || + astUtils.isSpecificMemberAccess(parent.callee, "Number", "parseInt") ); } @@ -173,7 +169,7 @@ module.exports = { return { Literal(node) { - if (!isNumericLiteral(node)) { + if (!astUtils.isNumericLiteral(node)) { return; } diff --git a/lib/rules/no-obj-calls.js b/lib/rules/no-obj-calls.js index 6139ba2c182..6eb200c9b87 100644 --- a/lib/rules/no-obj-calls.js +++ b/lib/rules/no-obj-calls.js @@ -24,10 +24,13 @@ const nonCallableGlobals = ["Atomics", "JSON", "Math", "Reflect"]; * @returns {string} name to report */ function getReportNodeName(node) { - if (node.callee.type === "MemberExpression") { - return getPropertyName(node.callee); + if (node.type === "ChainExpression") { + return getReportNodeName(node.expression); } - return node.callee.name; + if (node.type === "MemberExpression") { + return getPropertyName(node); + } + return node.name; } //------------------------------------------------------------------------------ @@ -69,7 +72,7 @@ module.exports = { } for (const { node, path } of tracker.iterateGlobalReferences(traceMap)) { - const name = getReportNodeName(node); + const name = getReportNodeName(node.callee); const ref = path[0]; const messageId = name === ref ? "unexpectedCall" : "unexpectedRefCall"; diff --git a/lib/rules/no-prototype-builtins.js b/lib/rules/no-prototype-builtins.js index a00d3707204..ccec86c30da 100644 --- a/lib/rules/no-prototype-builtins.js +++ b/lib/rules/no-prototype-builtins.js @@ -4,6 +4,12 @@ */ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -39,15 +45,19 @@ module.exports = { * @returns {void} */ function disallowBuiltIns(node) { - if (node.callee.type !== "MemberExpression" || node.callee.computed) { + + // TODO: just use `astUtils.getStaticPropertyName(node.callee)` + const callee = astUtils.skipChainExpression(node.callee); + + if (callee.type !== "MemberExpression" || callee.computed) { return; } - const propName = node.callee.property.name; + const propName = callee.property.name; if (DISALLOWED_PROPS.indexOf(propName) > -1) { context.report({ messageId: "prototypeBuildIn", - loc: node.callee.property.loc, + loc: callee.property.loc, data: { prop: propName }, node }); diff --git a/lib/rules/no-self-assign.js b/lib/rules/no-self-assign.js index 170e46b0593..705be324cf0 100644 --- a/lib/rules/no-self-assign.js +++ b/lib/rules/no-self-assign.js @@ -17,56 +17,6 @@ const astUtils = require("./utils/ast-utils"); const SPACES = /\s+/gu; -/** - * Checks whether the property of 2 given member expression nodes are the same - * property or not. - * @param {ASTNode} left A member expression node to check. - * @param {ASTNode} right Another member expression node to check. - * @returns {boolean} `true` if the member expressions have the same property. - */ -function isSameProperty(left, right) { - if (left.property.type === "Identifier" && - left.property.type === right.property.type && - left.property.name === right.property.name && - left.computed === right.computed - ) { - return true; - } - - const lname = astUtils.getStaticPropertyName(left); - const rname = astUtils.getStaticPropertyName(right); - - return lname !== null && lname === rname; -} - -/** - * Checks whether 2 given member expression nodes are the reference to the same - * property or not. - * @param {ASTNode} left A member expression node to check. - * @param {ASTNode} right Another member expression node to check. - * @returns {boolean} `true` if the member expressions are the reference to the - * same property or not. - */ -function isSameMember(left, right) { - if (!isSameProperty(left, right)) { - return false; - } - - const lobj = left.object; - const robj = right.object; - - if (lobj.type !== robj.type) { - return false; - } - if (lobj.type === "MemberExpression") { - return isSameMember(lobj, robj); - } - if (lobj.type === "ThisExpression") { - return true; - } - return lobj.type === "Identifier" && lobj.name === robj.name; -} - /** * Traverses 2 Pattern nodes in parallel, then reports self-assignments. * @param {ASTNode|null} left A left node to traverse. This is a Pattern or @@ -162,9 +112,9 @@ function eachSelfAssignment(left, right, props, report) { } } else if ( props && - left.type === "MemberExpression" && - right.type === "MemberExpression" && - isSameMember(left, right) + astUtils.skipChainExpression(left).type === "MemberExpression" && + astUtils.skipChainExpression(right).type === "MemberExpression" && + astUtils.isSameReference(left, right) ) { report(right); } diff --git a/lib/rules/no-setter-return.js b/lib/rules/no-setter-return.js index a558640c357..9c79240dda1 100644 --- a/lib/rules/no-setter-return.js +++ b/lib/rules/no-setter-return.js @@ -39,15 +39,12 @@ function isGlobalReference(node, scope) { * @returns {boolean} `true` if the node is argument at the given position. */ function isArgumentOfGlobalMethodCall(node, scope, objectName, methodName, index) { - const parent = node.parent; + const callNode = node.parent; - return parent.type === "CallExpression" && - parent.arguments[index] === node && - parent.callee.type === "MemberExpression" && - astUtils.getStaticPropertyName(parent.callee) === methodName && - parent.callee.object.type === "Identifier" && - parent.callee.object.name === objectName && - isGlobalReference(parent.callee.object, scope); + return callNode.type === "CallExpression" && + callNode.arguments[index] === node && + astUtils.isSpecificMemberAccess(callNode.callee, objectName, methodName) && + isGlobalReference(astUtils.skipChainExpression(callNode.callee).object, scope); } /** diff --git a/lib/rules/no-unexpected-multiline.js b/lib/rules/no-unexpected-multiline.js index b5ec20de4b2..7af3fe67090 100644 --- a/lib/rules/no-unexpected-multiline.js +++ b/lib/rules/no-unexpected-multiline.js @@ -68,7 +68,7 @@ module.exports = { return { MemberExpression(node) { - if (!node.computed) { + if (!node.computed || node.optional) { return; } checkForBreakAfter(node.object, "property"); @@ -96,7 +96,7 @@ module.exports = { }, CallExpression(node) { - if (node.arguments.length === 0) { + if (node.arguments.length === 0 || node.optional) { return; } checkForBreakAfter(node.callee, "function"); diff --git a/lib/rules/no-unneeded-ternary.js b/lib/rules/no-unneeded-ternary.js index 0fefc42b909..06c615f3824 100644 --- a/lib/rules/no-unneeded-ternary.js +++ b/lib/rules/no-unneeded-ternary.js @@ -122,7 +122,6 @@ module.exports = { if (isBooleanLiteral(node.alternate) && isBooleanLiteral(node.consequent)) { context.report({ node, - loc: node.consequent.loc.start, messageId: "unnecessaryConditionalExpression", fix(fixer) { if (node.consequent.value === node.alternate.value) { @@ -144,7 +143,6 @@ module.exports = { } else if (!defaultAssignment && matchesDefaultAssignment(node)) { context.report({ node, - loc: node.consequent.loc.start, messageId: "unnecessaryConditionalAssignment", fix: fixer => { const shouldParenthesizeAlternate = diff --git a/lib/rules/no-unused-expressions.js b/lib/rules/no-unused-expressions.js index 8c049f556ff..882a0fd1c11 100644 --- a/lib/rules/no-unused-expressions.js +++ b/lib/rules/no-unused-expressions.js @@ -8,6 +8,22 @@ // Rule Definition //------------------------------------------------------------------------------ +/** + * Returns `true`. + * @returns {boolean} `true`. + */ +function alwaysTrue() { + return true; +} + +/** + * Returns `false`. + * @returns {boolean} `false`. + */ +function alwaysFalse() { + return false; +} + module.exports = { meta: { type: "suggestion", @@ -101,40 +117,56 @@ module.exports = { } /** - * Determines whether or not a given node is a valid expression. Recurses on short circuit eval and ternary nodes if enabled by flags. - * @param {ASTNode} node any node - * @returns {boolean} whether the given node is a valid expression + * The member functions return `true` if the type has no side-effects. + * Unknown nodes are handled as `false`, then this rule ignores those. */ - function isValidExpression(node) { - if (allowTernary) { - - // Recursive check for ternary and logical expressions - if (node.type === "ConditionalExpression") { - return isValidExpression(node.consequent) && isValidExpression(node.alternate); + const Checker = Object.assign(Object.create(null), { + isDisallowed(node) { + return (Checker[node.type] || alwaysFalse)(node); + }, + + ArrayExpression: alwaysTrue, + ArrowFunctionExpression: alwaysTrue, + BinaryExpression: alwaysTrue, + ChainExpression(node) { + return Checker.isDisallowed(node.expression); + }, + ClassExpression: alwaysTrue, + ConditionalExpression(node) { + if (allowTernary) { + return Checker.isDisallowed(node.consequent) || Checker.isDisallowed(node.alternate); } - } - - if (allowShortCircuit) { - if (node.type === "LogicalExpression") { - return isValidExpression(node.right); + return true; + }, + FunctionExpression: alwaysTrue, + Identifier: alwaysTrue, + Literal: alwaysTrue, + LogicalExpression(node) { + if (allowShortCircuit) { + return Checker.isDisallowed(node.right); } - } - - if (allowTaggedTemplates && node.type === "TaggedTemplateExpression") { return true; + }, + MemberExpression: alwaysTrue, + MetaProperty: alwaysTrue, + ObjectExpression: alwaysTrue, + SequenceExpression: alwaysTrue, + TaggedTemplateExpression() { + return !allowTaggedTemplates; + }, + TemplateLiteral: alwaysTrue, + ThisExpression: alwaysTrue, + UnaryExpression(node) { + return node.operator !== "void" && node.operator !== "delete"; } - - return /^(?:Assignment|Call|New|Update|Yield|Await|Import)Expression$/u.test(node.type) || - (node.type === "UnaryExpression" && ["delete", "void"].indexOf(node.operator) >= 0); - } + }); return { ExpressionStatement(node) { - if (!isValidExpression(node.expression) && !isDirective(node, context.getAncestors())) { + if (Checker.isDisallowed(node.expression) && !isDirective(node, context.getAncestors())) { context.report({ node, messageId: "unusedExpression" }); } } }; - } }; diff --git a/lib/rules/no-useless-call.js b/lib/rules/no-useless-call.js index afc729d5de0..b1382a2fa28 100644 --- a/lib/rules/no-useless-call.js +++ b/lib/rules/no-useless-call.js @@ -17,13 +17,15 @@ const astUtils = require("./utils/ast-utils"); * @returns {boolean} Whether or not the node is a `.call()`/`.apply()`. */ function isCallOrNonVariadicApply(node) { + const callee = astUtils.skipChainExpression(node.callee); + return ( - node.callee.type === "MemberExpression" && - node.callee.property.type === "Identifier" && - node.callee.computed === false && + callee.type === "MemberExpression" && + callee.property.type === "Identifier" && + callee.computed === false && ( - (node.callee.property.name === "call" && node.arguments.length >= 1) || - (node.callee.property.name === "apply" && node.arguments.length === 2 && node.arguments[1].type === "ArrayExpression") + (callee.property.name === "call" && node.arguments.length >= 1) || + (callee.property.name === "apply" && node.arguments.length === 2 && node.arguments[1].type === "ArrayExpression") ) ); } @@ -74,12 +76,13 @@ module.exports = { return; } - const applied = node.callee.object; + const callee = astUtils.skipChainExpression(node.callee); + const applied = astUtils.skipChainExpression(callee.object); const expectedThis = (applied.type === "MemberExpression") ? applied.object : null; const thisArg = node.arguments[0]; if (isValidThisArg(expectedThis, thisArg, sourceCode)) { - context.report({ node, messageId: "unnecessaryCall", data: { name: node.callee.property.name } }); + context.report({ node, messageId: "unnecessaryCall", data: { name: callee.property.name } }); } } }; diff --git a/lib/rules/no-whitespace-before-property.js b/lib/rules/no-whitespace-before-property.js index ccd0b091b74..226f873c5f6 100644 --- a/lib/rules/no-whitespace-before-property.js +++ b/lib/rules/no-whitespace-before-property.js @@ -49,8 +49,6 @@ module.exports = { * @private */ function reportError(node, leftToken, rightToken) { - const replacementText = node.computed ? "" : "."; - context.report({ node, messageId: "unexpectedWhitespace", @@ -58,7 +56,9 @@ module.exports = { propName: sourceCode.getText(node.property) }, fix(fixer) { - if (!node.computed && astUtils.isDecimalInteger(node.object)) { + let replacementText = ""; + + if (!node.computed && !node.optional && astUtils.isDecimalInteger(node.object)) { /* * If the object is a number literal, fixing it to something like 5.toString() would cause a SyntaxError. @@ -66,6 +66,18 @@ module.exports = { */ return null; } + + // Don't fix if comments exist. + if (sourceCode.commentsExistBetween(leftToken, rightToken)) { + return null; + } + + if (node.optional) { + replacementText = "?."; + } else if (!node.computed) { + replacementText = "."; + } + return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], replacementText); } }); @@ -86,7 +98,7 @@ module.exports = { if (node.computed) { rightToken = sourceCode.getTokenBefore(node.property, astUtils.isOpeningBracketToken); - leftToken = sourceCode.getTokenBefore(rightToken); + leftToken = sourceCode.getTokenBefore(rightToken, node.optional ? 1 : 0); } else { rightToken = sourceCode.getFirstToken(node.property); leftToken = sourceCode.getTokenBefore(rightToken, 1); diff --git a/lib/rules/object-curly-newline.js b/lib/rules/object-curly-newline.js index b48b2526a0b..616d59851d6 100644 --- a/lib/rules/object-curly-newline.js +++ b/lib/rules/object-curly-newline.js @@ -224,7 +224,7 @@ module.exports = { context.report({ messageId: "expectedLinebreakAfterOpeningBrace", node, - loc: openBrace.loc.start, + loc: openBrace.loc, fix(fixer) { if (hasCommentsFirstToken) { return null; @@ -238,7 +238,7 @@ module.exports = { context.report({ messageId: "expectedLinebreakBeforeClosingBrace", node, - loc: closeBrace.loc.start, + loc: closeBrace.loc, fix(fixer) { if (hasCommentsLastToken) { return null; @@ -260,7 +260,7 @@ module.exports = { context.report({ messageId: "unexpectedLinebreakAfterOpeningBrace", node, - loc: openBrace.loc.start, + loc: openBrace.loc, fix(fixer) { if (hasCommentsFirstToken) { return null; @@ -280,7 +280,7 @@ module.exports = { context.report({ messageId: "unexpectedLinebreakBeforeClosingBrace", node, - loc: closeBrace.loc.start, + loc: closeBrace.loc, fix(fixer) { if (hasCommentsLastToken) { return null; diff --git a/lib/rules/operator-assignment.js b/lib/rules/operator-assignment.js index 6820793439c..aee79077f44 100644 --- a/lib/rules/operator-assignment.js +++ b/lib/rules/operator-assignment.js @@ -40,45 +40,6 @@ function isNonCommutativeOperatorWithShorthand(operator) { // Rule Definition //------------------------------------------------------------------------------ -/** - * Checks whether two expressions reference the same value. For example: - * a = a - * a.b = a.b - * a[0] = a[0] - * a['b'] = a['b'] - * @param {ASTNode} a Left side of the comparison. - * @param {ASTNode} b Right side of the comparison. - * @returns {boolean} True if both sides match and reference the same value. - */ -function same(a, b) { - if (a.type !== b.type) { - return false; - } - - switch (a.type) { - case "Identifier": - return a.name === b.name; - - case "Literal": - return a.value === b.value; - - case "MemberExpression": - - /* - * x[0] = x[0] - * x[y] = x[y] - * x.y = x.y - */ - return same(a.object, b.object) && same(a.property, b.property); - - case "ThisExpression": - return true; - - default: - return false; - } -} - /** * Determines if the left side of a node can be safely fixed (i.e. if it activates the same getters/setters and) * toString calls regardless of whether assignment shorthand is used) @@ -148,12 +109,12 @@ module.exports = { const operator = expr.operator; if (isCommutativeOperatorWithShorthand(operator) || isNonCommutativeOperatorWithShorthand(operator)) { - if (same(left, expr.left)) { + if (astUtils.isSameReference(left, expr.left, true)) { context.report({ node, messageId: "replaced", fix(fixer) { - if (canBeFixed(left)) { + if (canBeFixed(left) && canBeFixed(expr.left)) { const equalsToken = getOperatorToken(node); const operatorToken = getOperatorToken(expr); const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]); @@ -169,7 +130,7 @@ module.exports = { return null; } }); - } else if (same(left, expr.right) && isCommutativeOperatorWithShorthand(operator)) { + } else if (astUtils.isSameReference(left, expr.right, true) && isCommutativeOperatorWithShorthand(operator)) { /* * This case can't be fixed safely. diff --git a/lib/rules/padding-line-between-statements.js b/lib/rules/padding-line-between-statements.js index eea19f5ce58..c97b9956b71 100644 --- a/lib/rules/padding-line-between-statements.js +++ b/lib/rules/padding-line-between-statements.js @@ -85,10 +85,10 @@ function newNodeTypeTester(type) { */ function isIIFEStatement(node) { if (node.type === "ExpressionStatement") { - let call = node.expression; + let call = astUtils.skipChainExpression(node.expression); if (call.type === "UnaryExpression") { - call = call.argument; + call = astUtils.skipChainExpression(call.argument); } return call.type === "CallExpression" && astUtils.isFunction(call.callee); } diff --git a/lib/rules/prefer-arrow-callback.js b/lib/rules/prefer-arrow-callback.js index d4e0251940c..ee5cfe3c8c7 100644 --- a/lib/rules/prefer-arrow-callback.js +++ b/lib/rules/prefer-arrow-callback.js @@ -5,6 +5,8 @@ "use strict"; +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ @@ -66,6 +68,7 @@ function getCallbackInfo(node) { const retv = { isCallback: false, isLexicalThis: false }; let currentNode = node; let parent = node.parent; + let bound = false; while (currentNode) { switch (parent.type) { @@ -73,23 +76,34 @@ function getCallbackInfo(node) { // Checks parents recursively. case "LogicalExpression": + case "ChainExpression": case "ConditionalExpression": break; // Checks whether the parent node is `.bind(this)` call. case "MemberExpression": - if (parent.object === currentNode && + if ( + parent.object === currentNode && !parent.property.computed && parent.property.type === "Identifier" && - parent.property.name === "bind" && - parent.parent.type === "CallExpression" && - parent.parent.callee === parent + parent.property.name === "bind" ) { - retv.isLexicalThis = ( - parent.parent.arguments.length === 1 && - parent.parent.arguments[0].type === "ThisExpression" - ); - parent = parent.parent; + const maybeCallee = parent.parent.type === "ChainExpression" + ? parent.parent + : parent; + + if (astUtils.isCallee(maybeCallee)) { + if (!bound) { + bound = true; // Use only the first `.bind()` to make `isLexicalThis` value. + retv.isLexicalThis = ( + maybeCallee.parent.arguments.length === 1 && + maybeCallee.parent.arguments[0].type === "ThisExpression" + ); + } + parent = maybeCallee.parent; + } else { + return retv; + } } else { return retv; } @@ -272,7 +286,7 @@ module.exports = { context.report({ node, messageId: "preferArrowCallback", - fix(fixer) { + *fix(fixer) { if ((!callbackInfo.isLexicalThis && scopeInfo.this) || hasDuplicateParams(node.params)) { /* @@ -281,30 +295,81 @@ module.exports = { * If the callback function has duplicates in its list of parameters (possible in sloppy mode), * don't replace it with an arrow function, because this is a SyntaxError with arrow functions. */ - return null; + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive } - const paramsLeftParen = node.params.length ? sourceCode.getTokenBefore(node.params[0]) : sourceCode.getTokenBefore(node.body, 1); - const paramsRightParen = sourceCode.getTokenBefore(node.body); - const asyncKeyword = node.async ? "async " : ""; - const paramsFullText = sourceCode.text.slice(paramsLeftParen.range[0], paramsRightParen.range[1]); - const arrowFunctionText = `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`; + // Remove `.bind(this)` if exists. + if (callbackInfo.isLexicalThis) { + const memberNode = node.parent; - /* - * If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding. - * Otherwise, just replace the arrow function itself. - */ - const replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node; + /* + * If `.bind(this)` exists but the parent is not `.bind(this)`, don't remove it automatically. + * E.g. `(foo || function(){}).bind(this)` + */ + if (memberNode.type !== "MemberExpression") { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + } + + const callNode = memberNode.parent; + const firstTokenToRemove = sourceCode.getTokenAfter(memberNode.object, astUtils.isNotClosingParenToken); + const lastTokenToRemove = sourceCode.getLastToken(callNode); + + /* + * If the member expression is parenthesized, don't remove the right paren. + * E.g. `(function(){}.bind)(this)` + * ^^^^^^^^^^^^ + */ + if (astUtils.isParenthesised(sourceCode, memberNode)) { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + } + + // If comments exist in the `.bind(this)`, don't remove those. + if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + } + + yield fixer.removeRange([firstTokenToRemove.range[0], lastTokenToRemove.range[1]]); + } + + // Convert the function expression to an arrow function. + const functionToken = sourceCode.getFirstToken(node, node.async ? 1 : 0); + const leftParenToken = sourceCode.getTokenAfter(functionToken, astUtils.isOpeningParenToken); + + if (sourceCode.commentsExistBetween(functionToken, leftParenToken)) { + + // Remove only extra tokens to keep comments. + yield fixer.remove(functionToken); + if (node.id) { + yield fixer.remove(node.id); + } + } else { + + // Remove extra tokens and spaces. + yield fixer.removeRange([functionToken.range[0], leftParenToken.range[0]]); + } + yield fixer.insertTextBefore(node.body, "=> "); + + // Get the node that will become the new arrow function. + let replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node; + + if (replacedNode.type === "ChainExpression") { + replacedNode = replacedNode.parent; + } /* * If the replaced node is part of a BinaryExpression, LogicalExpression, or MemberExpression, then * the arrow function needs to be parenthesized, because `foo || () => {}` is invalid syntax even * though `foo || function() {}` is valid. */ - const needsParens = replacedNode.parent.type !== "CallExpression" && replacedNode.parent.type !== "ConditionalExpression"; - const replacementText = needsParens ? `(${arrowFunctionText})` : arrowFunctionText; - - return fixer.replaceText(replacedNode, replacementText); + if ( + replacedNode.parent.type !== "CallExpression" && + replacedNode.parent.type !== "ConditionalExpression" && + !astUtils.isParenthesised(sourceCode, replacedNode) && + !astUtils.isParenthesised(sourceCode, node) + ) { + yield fixer.insertTextBefore(replacedNode, "("); + yield fixer.insertTextAfter(replacedNode, ")"); + } } }); } diff --git a/lib/rules/prefer-exponentiation-operator.js b/lib/rules/prefer-exponentiation-operator.js index 5e75ef4724f..d1a00d6209e 100644 --- a/lib/rules/prefer-exponentiation-operator.js +++ b/lib/rules/prefer-exponentiation-operator.js @@ -52,7 +52,7 @@ function doesExponentNeedParens(exponent) { * @returns {boolean} `true` if the expression needs to be parenthesised. */ function doesExponentiationExpressionNeedParens(node, sourceCode) { - const parent = node.parent; + const parent = node.parent.type === "ChainExpression" ? node.parent.parent : node.parent; const needsParens = ( parent.type === "ClassDeclaration" || diff --git a/lib/rules/prefer-numeric-literals.js b/lib/rules/prefer-numeric-literals.js index 2a4fb5d954a..662136c4aad 100644 --- a/lib/rules/prefer-numeric-literals.js +++ b/lib/rules/prefer-numeric-literals.js @@ -29,19 +29,10 @@ const radixMap = new Map([ * false otherwise. */ function isParseInt(calleeNode) { - switch (calleeNode.type) { - case "Identifier": - return calleeNode.name === "parseInt"; - case "MemberExpression": - return calleeNode.object.type === "Identifier" && - calleeNode.object.name === "Number" && - calleeNode.property.type === "Identifier" && - calleeNode.property.name === "parseInt"; - - // no default - } - - return false; + return ( + astUtils.isSpecificId(calleeNode, "parseInt") || + astUtils.isSpecificMemberAccess(calleeNode, "Number", "parseInt") + ); } //------------------------------------------------------------------------------ diff --git a/lib/rules/prefer-promise-reject-errors.js b/lib/rules/prefer-promise-reject-errors.js index 56911b67adc..ec16e445555 100644 --- a/lib/rules/prefer-promise-reject-errors.js +++ b/lib/rules/prefer-promise-reject-errors.js @@ -73,9 +73,7 @@ module.exports = { * @returns {boolean} `true` if the call is a Promise.reject() call */ function isPromiseRejectCall(node) { - return node.callee.type === "MemberExpression" && - node.callee.object.type === "Identifier" && node.callee.object.name === "Promise" && - node.callee.property.type === "Identifier" && node.callee.property.name === "reject"; + return astUtils.isSpecificMemberAccess(node.callee, "Promise", "reject"); } //---------------------------------------------------------------------- diff --git a/lib/rules/prefer-regex-literals.js b/lib/rules/prefer-regex-literals.js index 47b2b090f82..9e8ce023547 100644 --- a/lib/rules/prefer-regex-literals.js +++ b/lib/rules/prefer-regex-literals.js @@ -25,6 +25,15 @@ function isStringLiteral(node) { return node.type === "Literal" && typeof node.value === "string"; } +/** + * Determines whether the given node is a regex literal. + * @param {ASTNode} node Node to check. + * @returns {boolean} True if the node is a regex literal. + */ +function isRegexLiteral(node) { + return node.type === "Literal" && Object.prototype.hasOwnProperty.call(node, "regex"); +} + /** * Determines whether the given node is a template literal without expressions. * @param {ASTNode} node Node to check. @@ -50,14 +59,28 @@ module.exports = { url: "https://eslint.org/docs/rules/prefer-regex-literals" }, - schema: [], + schema: [ + { + type: "object", + properties: { + disallowRedundantWrapping: { + type: "boolean", + default: false + } + }, + additionalProperties: false + } + ], messages: { - unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor." + unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor.", + unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.", + unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor." } }, create(context) { + const [{ disallowRedundantWrapping = false } = {}] = context.options; /** * Determines whether the given identifier node is a reference to a global variable. @@ -79,11 +102,8 @@ module.exports = { */ function isStringRawTaggedStaticTemplateLiteral(node) { return node.type === "TaggedTemplateExpression" && - node.tag.type === "MemberExpression" && - node.tag.object.type === "Identifier" && - node.tag.object.name === "String" && - isGlobalReference(node.tag.object) && - astUtils.getStaticPropertyName(node.tag) === "raw" && + astUtils.isSpecificMemberAccess(node.tag, "String", "raw") && + isGlobalReference(astUtils.skipChainExpression(node.tag).object) && isStaticTemplateLiteral(node.quasi); } @@ -98,6 +118,40 @@ module.exports = { isStringRawTaggedStaticTemplateLiteral(node); } + /** + * Determines whether the relevant arguments of the given are all static string literals. + * @param {ASTNode} node Node to check. + * @returns {boolean} True if all arguments are static strings. + */ + function hasOnlyStaticStringArguments(node) { + const args = node.arguments; + + if ((args.length === 1 || args.length === 2) && args.every(isStaticString)) { + return true; + } + + return false; + } + + /** + * Determines whether the arguments of the given node indicate that a regex literal is unnecessarily wrapped. + * @param {ASTNode} node Node to check. + * @returns {boolean} True if the node already contains a regex literal argument. + */ + function isUnnecessarilyWrappedRegexLiteral(node) { + const args = node.arguments; + + if (args.length === 1 && isRegexLiteral(args[0])) { + return true; + } + + if (args.length === 2 && isRegexLiteral(args[0]) && isStaticString(args[1])) { + return true; + } + + return false; + } + return { Program() { const scope = context.getScope(); @@ -110,12 +164,13 @@ module.exports = { }; for (const { node } of tracker.iterateGlobalReferences(traceMap)) { - const args = node.arguments; - - if ( - (args.length === 1 || args.length === 2) && - args.every(isStaticString) - ) { + if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(node)) { + if (node.arguments.length === 2) { + context.report({ node, messageId: "unexpectedRedundantRegExpWithFlags" }); + } else { + context.report({ node, messageId: "unexpectedRedundantRegExp" }); + } + } else if (hasOnlyStaticStringArguments(node)) { context.report({ node, messageId: "unexpectedRegExp" }); } } diff --git a/lib/rules/prefer-spread.js b/lib/rules/prefer-spread.js index bcb0dc0dd4c..d3c3c4d2297 100644 --- a/lib/rules/prefer-spread.js +++ b/lib/rules/prefer-spread.js @@ -18,17 +18,13 @@ const astUtils = require("./utils/ast-utils"); */ function isVariadicApplyCalling(node) { return ( - node.callee.type === "MemberExpression" && - node.callee.property.type === "Identifier" && - node.callee.property.name === "apply" && - node.callee.computed === false && + astUtils.isSpecificMemberAccess(node.callee, null, "apply") && node.arguments.length === 2 && node.arguments[1].type !== "ArrayExpression" && node.arguments[1].type !== "SpreadElement" ); } - /** * Checks whether or not `thisArg` is not changed by `.apply()`. * @param {ASTNode|null} expectedThis The node that is the owner of the applied function. @@ -75,7 +71,7 @@ module.exports = { return; } - const applied = node.callee.object; + const applied = astUtils.skipChainExpression(astUtils.skipChainExpression(node.callee).object); const expectedThis = (applied.type === "MemberExpression") ? applied.object : null; const thisArg = node.arguments[0]; diff --git a/lib/rules/radix.js b/lib/rules/radix.js index 3903cb2a6a2..e3225662388 100644 --- a/lib/rules/radix.js +++ b/lib/rules/radix.js @@ -166,9 +166,12 @@ module.exports = { if (variable && !isShadowed(variable)) { variable.references.forEach(reference => { const node = reference.identifier.parent; + const maybeCallee = node.parent.type === "ChainExpression" + ? node.parent + : node; - if (isParseIntMethod(node) && astUtils.isCallee(node)) { - checkArguments(node.parent); + if (isParseIntMethod(node) && astUtils.isCallee(maybeCallee)) { + checkArguments(maybeCallee.parent); } }); } diff --git a/lib/rules/sort-imports.js b/lib/rules/sort-imports.js index 65ad9a18a93..4c3ddec7669 100644 --- a/lib/rules/sort-imports.js +++ b/lib/rules/sort-imports.js @@ -44,6 +44,10 @@ module.exports = { ignoreMemberSort: { type: "boolean", default: false + }, + allowSeparatedGroups: { + type: "boolean", + default: false } }, additionalProperties: false @@ -66,6 +70,7 @@ module.exports = { ignoreDeclarationSort = configuration.ignoreDeclarationSort || false, ignoreMemberSort = configuration.ignoreMemberSort || false, memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"], + allowSeparatedGroups = configuration.allowSeparatedGroups || false, sourceCode = context.getSourceCode(); let previousDeclaration = null; @@ -115,9 +120,32 @@ module.exports = { } + /** + * Calculates number of lines between two nodes. It is assumed that the given `left` node appears before + * the given `right` node in the source code. Lines are counted from the end of the `left` node till the + * start of the `right` node. If the given nodes are on the same line, it returns `0`, same as if they were + * on two consecutive lines. + * @param {ASTNode} left node that appears before the given `right` node. + * @param {ASTNode} right node that appears after the given `left` node. + * @returns {number} number of lines between nodes. + */ + function getNumberOfLinesBetween(left, right) { + return Math.max(right.loc.start.line - left.loc.end.line - 1, 0); + } + return { ImportDeclaration(node) { if (!ignoreDeclarationSort) { + if ( + previousDeclaration && + allowSeparatedGroups && + getNumberOfLinesBetween(previousDeclaration, node) > 0 + ) { + + // reset declaration sort + previousDeclaration = null; + } + if (previousDeclaration) { const currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node), previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration); diff --git a/lib/rules/use-isnan.js b/lib/rules/use-isnan.js index 7b466be75f2..53ffeb7e6d1 100644 --- a/lib/rules/use-isnan.js +++ b/lib/rules/use-isnan.js @@ -106,7 +106,7 @@ module.exports = { * @returns {void} */ function checkCallExpression(node) { - const callee = node.callee; + const callee = astUtils.skipChainExpression(node.callee); if (callee.type === "MemberExpression") { const methodName = astUtils.getStaticPropertyName(callee); diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index ecea6948da2..d0dd770d199 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -143,6 +143,23 @@ function isInLoop(node) { return false; } +/** + * Determines whether the given node is a `null` literal. + * @param {ASTNode} node The node to check + * @returns {boolean} `true` if the node is a `null` literal + */ +function isNullLiteral(node) { + + /* + * Checking `node.value === null` does not guarantee that a literal is a null literal. + * When parsing values that cannot be represented in the current environment (e.g. unicode + * regexes in Node 4), `node.value` is set to `null` because it wouldn't be possible to + * set `node.value` to a unicode regex. To make sure a literal is actually `null`, check + * `node.regex` instead. Also see: https://github.com/eslint/eslint/issues/8020 + */ + return node.type === "Literal" && node.value === null && !node.regex && !node.bigint; +} + /** * Checks whether or not a node is `null` or `undefined`. * @param {ASTNode} node A node to check. @@ -151,7 +168,7 @@ function isInLoop(node) { */ function isNullOrUndefined(node) { return ( - module.exports.isNullLiteral(node) || + isNullLiteral(node) || (node.type === "Identifier" && node.name === "undefined") || (node.type === "UnaryExpression" && node.operator === "void") ); @@ -166,20 +183,270 @@ function isCallee(node) { return node.parent.type === "CallExpression" && node.parent.callee === node; } +/** + * Returns the result of the string conversion applied to the evaluated value of the given expression node, + * if it can be determined statically. + * + * This function returns a `string` value for all `Literal` nodes and simple `TemplateLiteral` nodes only. + * In all other cases, this function returns `null`. + * @param {ASTNode} node Expression node. + * @returns {string|null} String value if it can be determined. Otherwise, `null`. + */ +function getStaticStringValue(node) { + switch (node.type) { + case "Literal": + if (node.value === null) { + if (isNullLiteral(node)) { + return String(node.value); // "null" + } + if (node.regex) { + return `/${node.regex.pattern}/${node.regex.flags}`; + } + if (node.bigint) { + return node.bigint; + } + + // Otherwise, this is an unknown literal. The function will return null. + + } else { + return String(node.value); + } + break; + case "TemplateLiteral": + if (node.expressions.length === 0 && node.quasis.length === 1) { + return node.quasis[0].value.cooked; + } + break; + + // no default + } + + return null; +} + +/** + * Gets the property name of a given node. + * The node can be a MemberExpression, a Property, or a MethodDefinition. + * + * If the name is dynamic, this returns `null`. + * + * For examples: + * + * a.b // => "b" + * a["b"] // => "b" + * a['b'] // => "b" + * a[`b`] // => "b" + * a[100] // => "100" + * a[b] // => null + * a["a" + "b"] // => null + * a[tag`b`] // => null + * a[`${b}`] // => null + * + * let a = {b: 1} // => "b" + * let a = {["b"]: 1} // => "b" + * let a = {['b']: 1} // => "b" + * let a = {[`b`]: 1} // => "b" + * let a = {[100]: 1} // => "100" + * let a = {[b]: 1} // => null + * let a = {["a" + "b"]: 1} // => null + * let a = {[tag`b`]: 1} // => null + * let a = {[`${b}`]: 1} // => null + * @param {ASTNode} node The node to get. + * @returns {string|null} The property name if static. Otherwise, null. + */ +function getStaticPropertyName(node) { + let prop; + + switch (node && node.type) { + case "ChainExpression": + return getStaticPropertyName(node.expression); + + case "Property": + case "MethodDefinition": + prop = node.key; + break; + + case "MemberExpression": + prop = node.property; + break; + + // no default + } + + if (prop) { + if (prop.type === "Identifier" && !node.computed) { + return prop.name; + } + + return getStaticStringValue(prop); + } + + return null; +} + +/** + * Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it. + * @param {ASTNode} node The node to address. + * @returns {ASTNode} The `ChainExpression#expression` value if the node is a `ChainExpression` node. Otherwise, the node. + */ +function skipChainExpression(node) { + return node && node.type === "ChainExpression" ? node.expression : node; +} + +/** + * Check if the `actual` is an expected value. + * @param {string} actual The string value to check. + * @param {string | RegExp} expected The expected string value or pattern. + * @returns {boolean} `true` if the `actual` is an expected value. + */ +function checkText(actual, expected) { + return typeof expected === "string" + ? actual === expected + : expected.test(actual); +} + +/** + * Check if a given node is an Identifier node with a given name. + * @param {ASTNode} node The node to check. + * @param {string | RegExp} name The expected name or the expected pattern of the object name. + * @returns {boolean} `true` if the node is an Identifier node with the name. + */ +function isSpecificId(node, name) { + return node.type === "Identifier" && checkText(node.name, name); +} + +/** + * Check if a given node is member access with a given object name and property name pair. + * This is regardless of optional or not. + * @param {ASTNode} node The node to check. + * @param {string | RegExp | null} objectName The expected name or the expected pattern of the object name. If this is nullish, this method doesn't check object. + * @param {string | RegExp | null} propertyName The expected name or the expected pattern of the property name. If this is nullish, this method doesn't check property. + * @returns {boolean} `true` if the node is member access with the object name and property name pair. + * The node is a `MemberExpression` or `ChainExpression`. + */ +function isSpecificMemberAccess(node, objectName, propertyName) { + const checkNode = skipChainExpression(node); + + if (checkNode.type !== "MemberExpression") { + return false; + } + + if (objectName && !isSpecificId(checkNode.object, objectName)) { + return false; + } + + if (propertyName) { + const actualPropertyName = getStaticPropertyName(checkNode); + + if (typeof actualPropertyName !== "string" || !checkText(actualPropertyName, propertyName)) { + return false; + } + } + + return true; +} + +/** + * Check if two literal nodes are the same value. + * @param {ASTNode} left The Literal node to compare. + * @param {ASTNode} right The other Literal node to compare. + * @returns {boolean} `true` if the two literal nodes are the same value. + */ +function equalLiteralValue(left, right) { + + // RegExp literal. + if (left.regex || right.regex) { + return Boolean( + left.regex && + right.regex && + left.regex.pattern === right.regex.pattern && + left.regex.flags === right.regex.flags + ); + } + + // BigInt literal. + if (left.bigint || right.bigint) { + return left.bigint === right.bigint; + } + + return left.value === right.value; +} + +/** + * Check if two expressions reference the same value. For example: + * a = a + * a.b = a.b + * a[0] = a[0] + * a['b'] = a['b'] + * @param {ASTNode} left The left side of the comparison. + * @param {ASTNode} right The right side of the comparison. + * @param {boolean} [disableStaticComputedKey] Don't address `a.b` and `a["b"]` are the same if `true`. For backward compatibility. + * @returns {boolean} `true` if both sides match and reference the same value. + */ +function isSameReference(left, right, disableStaticComputedKey = false) { + if (left.type !== right.type) { + + // Handle `a.b` and `a?.b` are samely. + if (left.type === "ChainExpression") { + return isSameReference(left.expression, right, disableStaticComputedKey); + } + if (right.type === "ChainExpression") { + return isSameReference(left, right.expression, disableStaticComputedKey); + } + + return false; + } + + switch (left.type) { + case "Super": + case "ThisExpression": + return true; + + case "Identifier": + return left.name === right.name; + case "Literal": + return equalLiteralValue(left, right); + + case "ChainExpression": + return isSameReference(left.expression, right.expression, disableStaticComputedKey); + + case "MemberExpression": { + if (!disableStaticComputedKey) { + const nameA = getStaticPropertyName(left); + + // x.y = x["y"] + if (nameA !== null) { + return ( + isSameReference(left.object, right.object, disableStaticComputedKey) && + nameA === getStaticPropertyName(right) + ); + } + } + + /* + * x[0] = x[0] + * x[y] = x[y] + * x.y = x.y + */ + return ( + left.computed === right.computed && + isSameReference(left.object, right.object, disableStaticComputedKey) && + isSameReference(left.property, right.property, disableStaticComputedKey) + ); + } + + default: + return false; + } +} + /** * Checks whether or not a node is `Reflect.apply`. * @param {ASTNode} node A node to check. * @returns {boolean} Whether or not the node is a `Reflect.apply`. */ function isReflectApply(node) { - return ( - node.type === "MemberExpression" && - node.object.type === "Identifier" && - node.object.name === "Reflect" && - node.property.type === "Identifier" && - node.property.name === "apply" && - node.computed === false - ); + return isSpecificMemberAccess(node, "Reflect", "apply"); } /** @@ -188,14 +455,7 @@ function isReflectApply(node) { * @returns {boolean} Whether or not the node is a `Array.from`. */ function isArrayFromMethod(node) { - return ( - node.type === "MemberExpression" && - node.object.type === "Identifier" && - arrayOrTypedArrayPattern.test(node.object.name) && - node.property.type === "Identifier" && - node.property.name === "from" && - node.computed === false - ); + return isSpecificMemberAccess(node, arrayOrTypedArrayPattern, "from"); } /** @@ -204,17 +464,7 @@ function isArrayFromMethod(node) { * @returns {boolean} Whether or not the node is a method which has `thisArg`. */ function isMethodWhichHasThisArg(node) { - for ( - let currentNode = node; - currentNode.type === "MemberExpression" && !currentNode.computed; - currentNode = currentNode.property - ) { - if (currentNode.property.type === "Identifier") { - return arrayMethodPattern.test(currentNode.property.name); - } - } - - return false; + return isSpecificMemberAccess(node, null, arrayMethodPattern); } /** @@ -289,6 +539,15 @@ function isDotToken(token) { return token.value === "." && token.type === "Punctuator"; } +/** + * Checks if the given token is a `?.` token or not. + * @param {Token} token The token to check. + * @returns {boolean} `true` if the token is a `?.` token. + */ +function isQuestionDotToken(token) { + return token.value === "?." && token.type === "Punctuator"; +} + /** * Checks if the given token is a semicolon token or not. * @param {Token} token The token to check. @@ -505,6 +764,7 @@ module.exports = { isCommaToken, isCommentToken, isDotToken, + isQuestionDotToken, isKeywordToken, isNotClosingBraceToken: negate(isClosingBraceToken), isNotClosingBracketToken: negate(isClosingBracketToken), @@ -512,6 +772,7 @@ module.exports = { isNotColonToken: negate(isColonToken), isNotCommaToken: negate(isCommaToken), isNotDotToken: negate(isDotToken), + isNotQuestionDotToken: negate(isQuestionDotToken), isNotOpeningBraceToken: negate(isOpeningBraceToken), isNotOpeningBracketToken: negate(isOpeningBracketToken), isNotOpeningParenToken: negate(isOpeningParenToken), @@ -669,6 +930,7 @@ module.exports = { */ case "LogicalExpression": case "ConditionalExpression": + case "ChainExpression": currentNode = parent; break; @@ -755,14 +1017,21 @@ module.exports = { * (function foo() { ... }).apply(obj, []); */ case "MemberExpression": - return ( - parent.object !== currentNode || - parent.property.type !== "Identifier" || - !bindOrCallOrApplyPattern.test(parent.property.name) || - !isCallee(parent) || - parent.parent.arguments.length === 0 || - isNullOrUndefined(parent.parent.arguments[0]) - ); + if ( + parent.object === currentNode && + isSpecificMemberAccess(parent, null, bindOrCallOrApplyPattern) + ) { + const maybeCalleeNode = parent.parent.type === "ChainExpression" + ? parent.parent + : parent; + + return !( + isCallee(maybeCalleeNode) && + maybeCalleeNode.parent.arguments.length >= 1 && + !isNullOrUndefined(maybeCalleeNode.parent.arguments[0]) + ); + } + return true; /* * e.g. @@ -884,6 +1153,7 @@ module.exports = { return 17; case "CallExpression": + case "ChainExpression": case "ImportExpression": return 18; @@ -913,104 +1183,6 @@ module.exports = { return isFunction(node) && module.exports.isEmptyBlock(node.body); }, - /** - * Returns the result of the string conversion applied to the evaluated value of the given expression node, - * if it can be determined statically. - * - * This function returns a `string` value for all `Literal` nodes and simple `TemplateLiteral` nodes only. - * In all other cases, this function returns `null`. - * @param {ASTNode} node Expression node. - * @returns {string|null} String value if it can be determined. Otherwise, `null`. - */ - getStaticStringValue(node) { - switch (node.type) { - case "Literal": - if (node.value === null) { - if (module.exports.isNullLiteral(node)) { - return String(node.value); // "null" - } - if (node.regex) { - return `/${node.regex.pattern}/${node.regex.flags}`; - } - if (node.bigint) { - return node.bigint; - } - - // Otherwise, this is an unknown literal. The function will return null. - - } else { - return String(node.value); - } - break; - case "TemplateLiteral": - if (node.expressions.length === 0 && node.quasis.length === 1) { - return node.quasis[0].value.cooked; - } - break; - - // no default - } - - return null; - }, - - /** - * Gets the property name of a given node. - * The node can be a MemberExpression, a Property, or a MethodDefinition. - * - * If the name is dynamic, this returns `null`. - * - * For examples: - * - * a.b // => "b" - * a["b"] // => "b" - * a['b'] // => "b" - * a[`b`] // => "b" - * a[100] // => "100" - * a[b] // => null - * a["a" + "b"] // => null - * a[tag`b`] // => null - * a[`${b}`] // => null - * - * let a = {b: 1} // => "b" - * let a = {["b"]: 1} // => "b" - * let a = {['b']: 1} // => "b" - * let a = {[`b`]: 1} // => "b" - * let a = {[100]: 1} // => "100" - * let a = {[b]: 1} // => null - * let a = {["a" + "b"]: 1} // => null - * let a = {[tag`b`]: 1} // => null - * let a = {[`${b}`]: 1} // => null - * @param {ASTNode} node The node to get. - * @returns {string|null} The property name if static. Otherwise, null. - */ - getStaticPropertyName(node) { - let prop; - - switch (node && node.type) { - case "Property": - case "MethodDefinition": - prop = node.key; - break; - - case "MemberExpression": - prop = node.property; - break; - - // no default - } - - if (prop) { - if (prop.type === "Identifier" && !node.computed) { - return prop.name; - } - - return module.exports.getStaticStringValue(prop); - } - - return null; - }, - /** * Get directives from directive prologue of a Program or Function node. * @param {ASTNode} node The node to check. @@ -1164,7 +1336,7 @@ module.exports = { if (node.id) { tokens.push(`'${node.id.name}'`); } else { - const name = module.exports.getStaticPropertyName(parent); + const name = getStaticPropertyName(parent); if (name !== null) { tokens.push(`'${name}'`); @@ -1391,6 +1563,7 @@ module.exports = { case "TaggedTemplateExpression": case "YieldExpression": case "AwaitExpression": + case "ChainExpression": return true; // possibly an error object. case "AssignmentExpression": @@ -1413,23 +1586,6 @@ module.exports = { } }, - /** - * Determines whether the given node is a `null` literal. - * @param {ASTNode} node The node to check - * @returns {boolean} `true` if the node is a `null` literal - */ - isNullLiteral(node) { - - /* - * Checking `node.value === null` does not guarantee that a literal is a null literal. - * When parsing values that cannot be represented in the current environment (e.g. unicode - * regexes in Node 4), `node.value` is set to `null` because it wouldn't be possible to - * set `node.value` to a unicode regex. To make sure a literal is actually `null`, check - * `node.regex` instead. Also see: https://github.com/eslint/eslint/issues/8020 - */ - return node.type === "Literal" && node.value === null && !node.regex && !node.bigint; - }, - /** * Check if a given node is a numeric literal or not. * @param {ASTNode} node The node to check. @@ -1590,5 +1746,13 @@ module.exports = { isLogicalExpression, isCoalesceExpression, - isMixedLogicalAndCoalesceExpressions + isMixedLogicalAndCoalesceExpressions, + isNullLiteral, + getStaticStringValue, + getStaticPropertyName, + skipChainExpression, + isSpecificId, + isSpecificMemberAccess, + equalLiteralValue, + isSameReference }; diff --git a/lib/rules/wrap-iife.js b/lib/rules/wrap-iife.js index 896aed63de5..07e5b84a4a5 100644 --- a/lib/rules/wrap-iife.js +++ b/lib/rules/wrap-iife.js @@ -23,7 +23,14 @@ const eslintUtils = require("eslint-utils"); * @private */ function isCalleeOfNewExpression(node) { - return node.parent.type === "NewExpression" && node.parent.callee === node; + const maybeCallee = node.parent.type === "ChainExpression" + ? node.parent + : node; + + return ( + maybeCallee.parent.type === "NewExpression" && + maybeCallee.parent.callee === maybeCallee + ); } //------------------------------------------------------------------------------ @@ -98,7 +105,7 @@ module.exports = { * @returns {ASTNode} node that is the function expression of the given IIFE, or null if none exist */ function getFunctionNodeFromIIFE(node) { - const callee = node.callee; + const callee = astUtils.skipChainExpression(node.callee); if (callee.type === "FunctionExpression") { return callee; diff --git a/lib/rules/yoda.js b/lib/rules/yoda.js index f1159e5255d..87dd5ed6c57 100644 --- a/lib/rules/yoda.js +++ b/lib/rules/yoda.js @@ -111,59 +111,6 @@ function getNormalizedLiteral(node) { return null; } -/** - * Checks whether two expressions reference the same value. For example: - * a = a - * a.b = a.b - * a[0] = a[0] - * a['b'] = a['b'] - * @param {ASTNode} a Left side of the comparison. - * @param {ASTNode} b Right side of the comparison. - * @returns {boolean} True if both sides match and reference the same value. - */ -function same(a, b) { - if (a.type !== b.type) { - return false; - } - - switch (a.type) { - case "Identifier": - return a.name === b.name; - - case "Literal": - return a.value === b.value; - - case "MemberExpression": { - const nameA = astUtils.getStaticPropertyName(a); - - // x.y = x["y"] - if (nameA !== null) { - return ( - same(a.object, b.object) && - nameA === astUtils.getStaticPropertyName(b) - ); - } - - /* - * x[0] = x[0] - * x[y] = x[y] - * x.y = x.y - */ - return ( - a.computed === b.computed && - same(a.object, b.object) && - same(a.property, b.property) - ); - } - - case "ThisExpression": - return true; - - default: - return false; - } -} - //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -236,7 +183,7 @@ module.exports = { * @returns {boolean} Whether node is a "between" range test. */ function isBetweenTest() { - if (node.operator === "&&" && same(left.right, right.left)) { + if (node.operator === "&&" && astUtils.isSameReference(left.right, right.left)) { const leftLiteral = getNormalizedLiteral(left.left); const rightLiteral = getNormalizedLiteral(right.right); @@ -260,7 +207,7 @@ module.exports = { * @returns {boolean} Whether node is an "outside" range test. */ function isOutsideTest() { - if (node.operator === "||" && same(left.left, right.right)) { + if (node.operator === "||" && astUtils.isSameReference(left.left, right.right)) { const leftLiteral = getNormalizedLiteral(left.right); const rightLiteral = getNormalizedLiteral(right.left); diff --git a/messages/extend-config-missing.txt b/messages/extend-config-missing.txt index f7c5f71ebe3..4defd7ac4d1 100644 --- a/messages/extend-config-missing.txt +++ b/messages/extend-config-missing.txt @@ -2,4 +2,4 @@ ESLint couldn't find the config "<%- configName %>" to extend from. Please check The config "<%- configName %>" was referenced from the config file in "<%- importerName %>". -If you still have problems, please stop by https://eslint.org/chat to chat with the team. +If you still have problems, please stop by https://eslint.org/chat/help to chat with the team. diff --git a/messages/no-config-found.txt b/messages/no-config-found.txt index f1f7beb63b1..b46a7e5a7a6 100644 --- a/messages/no-config-found.txt +++ b/messages/no-config-found.txt @@ -4,4 +4,4 @@ ESLint couldn't find a configuration file. To set up a configuration file for th ESLint looked for configuration files in <%= directoryPath %> and its ancestors. If it found none, it then looked in your home directory. -If you think you already have a configuration file or if you need more help, please stop by the ESLint chat room: https://eslint.org/chat +If you think you already have a configuration file or if you need more help, please stop by the ESLint chat room: https://eslint.org/chat/help diff --git a/messages/plugin-conflict.txt b/messages/plugin-conflict.txt index f8b60631c58..3ab4b340ef2 100644 --- a/messages/plugin-conflict.txt +++ b/messages/plugin-conflict.txt @@ -4,4 +4,4 @@ ESLint couldn't determine the plugin "<%- pluginId %>" uniquely. Please remove the "plugins" setting from either config or remove either plugin installation. -If you still can't figure out the problem, please stop by https://eslint.org/chat to chat with the team. +If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. diff --git a/messages/plugin-missing.txt b/messages/plugin-missing.txt index 3d376733085..aa25f59ac44 100644 --- a/messages/plugin-missing.txt +++ b/messages/plugin-missing.txt @@ -8,4 +8,4 @@ It's likely that the plugin isn't installed correctly. Try reinstalling by runni The plugin "<%- pluginName %>" was referenced from the config file in "<%- importerName %>". -If you still can't figure out the problem, please stop by https://eslint.org/chat to chat with the team. +If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. diff --git a/messages/whitespace-found.txt b/messages/whitespace-found.txt index 7d72149a8fd..3eed1af5866 100644 --- a/messages/whitespace-found.txt +++ b/messages/whitespace-found.txt @@ -1,3 +1,3 @@ ESLint couldn't find the plugin "<%- pluginName %>". because there is whitespace in the name. Please check your configuration and remove all whitespace from the plugin name. -If you still can't figure out the problem, please stop by https://eslint.org/chat to chat with the team. +If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. diff --git a/package.json b/package.json index 46154d1ab3e..4433760134f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "7.3.0", + "version": "7.6.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { @@ -54,9 +54,9 @@ "doctrine": "^3.0.0", "enquirer": "^2.3.5", "eslint-scope": "^5.1.0", - "eslint-utils": "^2.0.0", - "eslint-visitor-keys": "^1.2.0", - "espree": "^7.1.0", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^1.3.0", + "espree": "^7.2.0", "esquery": "^1.2.0", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", @@ -70,7 +70,7 @@ "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", - "lodash": "^4.17.14", + "lodash": "^4.17.19", "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", @@ -111,7 +111,6 @@ "karma-mocha": "^1.3.0", "karma-mocha-reporter": "^2.2.3", "karma-webpack": "^4.0.0-rc.6", - "leche": "^2.2.3", "lint-staged": "^10.1.2", "load-perf": "^0.2.0", "markdownlint": "^0.19.0", @@ -122,7 +121,7 @@ "npm-license": "^0.3.3", "nyc": "^15.0.1", "proxyquire": "^2.0.1", - "puppeteer": "^2.1.1", + "puppeteer": "^4.0.0", "recast": "^0.19.0", "regenerator-runtime": "^0.13.2", "shelljs": "^0.8.2", diff --git a/tests/fixtures/code-path-analysis/logical--if-qdot-1.js b/tests/fixtures/code-path-analysis/logical--if-qdot-1.js new file mode 100644 index 00000000000..4e1d7b000c7 --- /dev/null +++ b/tests/fixtures/code-path-analysis/logical--if-qdot-1.js @@ -0,0 +1,38 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_4->s1_5->s1_6->s1_7->s1_9->s1_11; +s1_1->s1_3->s1_10->s1_11; +s1_4->s1_6->s1_8->s1_9; +s1_11->final; +*/ +if (obj?.foo) { + if (obj?.bar) { + foo(); + } else { + bar(); + } +} else { + qiz(); +} + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nIfStatement:enter\nChainExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="Identifier (foo)\nMemberExpression:exit"]; + s1_3[label="ChainExpression:exit"]; + s1_4[label="BlockStatement:enter\nIfStatement:enter\nChainExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_5[label="Identifier (bar)\nMemberExpression:exit"]; + s1_6[label="ChainExpression:exit"]; + s1_7[label="BlockStatement:enter\nExpressionStatement:enter\nCallExpression:enter\nIdentifier (foo)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"]; + s1_9[label="IfStatement:exit\nBlockStatement:exit"]; + s1_11[label="IfStatement:exit\nProgram:exit"]; + s1_10[label="BlockStatement:enter\nExpressionStatement:enter\nCallExpression:enter\nIdentifier (qiz)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"]; + s1_8[label="BlockStatement:enter\nExpressionStatement:enter\nCallExpression:enter\nIdentifier (bar)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"]; + initial->s1_1->s1_2->s1_3->s1_4->s1_5->s1_6->s1_7->s1_9->s1_11; + s1_1->s1_3->s1_10->s1_11; + s1_4->s1_6->s1_8->s1_9; + s1_11->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--complex-1.js b/tests/fixtures/code-path-analysis/optional-chaining--complex-1.js new file mode 100644 index 00000000000..5444269a841 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--complex-1.js @@ -0,0 +1,38 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_9->s1_10->s1_11->s1_12->s1_13->s1_16; +s1_1->s1_16; +s1_2->s1_4->s1_5->s1_16; +s1_6->s1_8->s1_16; +s1_9->s1_11->s1_13; +s1_16->final; +*/ + +obj?.[cond ? k1 : k2]?.[k3 || k4]?.(a1 && a2, b1 ?? b2).foo(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nCallExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="ConditionalExpression:enter\nIdentifier (cond)"]; + s1_3[label="Identifier (k1)"]; + s1_5[label="ConditionalExpression:exit\nMemberExpression:exit"]; + s1_6[label="LogicalExpression:enter\nIdentifier (k3)"]; + s1_7[label="Identifier (k4)"]; + s1_8[label="LogicalExpression:exit\nMemberExpression:exit"]; + s1_9[label="LogicalExpression:enter\nIdentifier (a1)"]; + s1_10[label="Identifier (a2)"]; + s1_11[label="LogicalExpression:exit\nLogicalExpression:enter\nIdentifier (b1)"]; + s1_12[label="Identifier (b2)"]; + s1_13[label="LogicalExpression:exit\nCallExpression:exit\nIdentifier (foo)\nMemberExpression:exit\nIdentifier (arg)\nCallExpression:exit"]; + s1_16[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + s1_4[label="Identifier (k2)"]; + initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_9->s1_10->s1_11->s1_12->s1_13->s1_16; + s1_1->s1_16; + s1_2->s1_4->s1_5->s1_16; + s1_6->s1_8->s1_16; + s1_9->s1_11->s1_13; + s1_16->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--complex-2.js b/tests/fixtures/code-path-analysis/optional-chaining--complex-2.js new file mode 100644 index 00000000000..8d53c07e5f5 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--complex-2.js @@ -0,0 +1,38 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_9->s1_10->s1_11->s1_12->s1_13->s1_16; +s1_1->s1_16; +s1_2->s1_4->s1_5->s1_16; +s1_6->s1_8->s1_16; +s1_9->s1_11->s1_13; +s1_16->final; +*/ + +(obj?.[cond ? k1 : k2]?.[k3 || k4]?.(a1 && a2, b1 ?? b2)).foo(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nCallExpression:enter\nMemberExpression:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="ConditionalExpression:enter\nIdentifier (cond)"]; + s1_3[label="Identifier (k1)"]; + s1_5[label="ConditionalExpression:exit\nMemberExpression:exit"]; + s1_6[label="LogicalExpression:enter\nIdentifier (k3)"]; + s1_7[label="Identifier (k4)"]; + s1_8[label="LogicalExpression:exit\nMemberExpression:exit"]; + s1_9[label="LogicalExpression:enter\nIdentifier (a1)"]; + s1_10[label="Identifier (a2)"]; + s1_11[label="LogicalExpression:exit\nLogicalExpression:enter\nIdentifier (b1)"]; + s1_12[label="Identifier (b2)"]; + s1_13[label="LogicalExpression:exit\nCallExpression:exit"]; + s1_16[label="ChainExpression:exit\nIdentifier (foo)\nMemberExpression:exit\nIdentifier (arg)\nCallExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + s1_4[label="Identifier (k2)"]; + initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_9->s1_10->s1_11->s1_12->s1_13->s1_16; + s1_1->s1_16; + s1_2->s1_4->s1_5->s1_16; + s1_6->s1_8->s1_16; + s1_9->s1_11->s1_13; + s1_16->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--complex-3.js b/tests/fixtures/code-path-analysis/optional-chaining--complex-3.js new file mode 100644 index 00000000000..0afef1cd172 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--complex-3.js @@ -0,0 +1,41 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_10->s1_11->s1_12->s1_13->s1_14->s1_15->s1_16; +s1_1->s1_10; +s1_2->s1_4->s1_5->s1_10; +s1_6->s1_8; +s1_10->s1_16; +s1_11->s1_13->s1_15; +s1_16->final; +*/ + +(obj?.[cond ? k1 : k2]?.[k3 || k4])?.(a1 && a2, b1 ?? b2).foo(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nCallExpression:enter\nChainExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="ConditionalExpression:enter\nIdentifier (cond)"]; + s1_3[label="Identifier (k1)"]; + s1_5[label="ConditionalExpression:exit\nMemberExpression:exit"]; + s1_6[label="LogicalExpression:enter\nIdentifier (k3)"]; + s1_7[label="Identifier (k4)"]; + s1_8[label="LogicalExpression:exit\nMemberExpression:exit"]; + s1_10[label="ChainExpression:exit"]; + s1_11[label="LogicalExpression:enter\nIdentifier (a1)"]; + s1_12[label="Identifier (a2)"]; + s1_13[label="LogicalExpression:exit\nLogicalExpression:enter\nIdentifier (b1)"]; + s1_14[label="Identifier (b2)"]; + s1_15[label="LogicalExpression:exit\nCallExpression:exit\nIdentifier (foo)\nMemberExpression:exit\nIdentifier (arg)\nCallExpression:exit"]; + s1_16[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + s1_4[label="Identifier (k2)"]; + initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_10->s1_11->s1_12->s1_13->s1_14->s1_15->s1_16; + s1_1->s1_10; + s1_2->s1_4->s1_5->s1_10; + s1_6->s1_8; + s1_10->s1_16; + s1_11->s1_13->s1_15; + s1_16->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--simple-1.js b/tests/fixtures/code-path-analysis/optional-chaining--simple-1.js new file mode 100644 index 00000000000..d466a4e72d7 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--simple-1.js @@ -0,0 +1,19 @@ +/*expected +initial->s1_1->s1_2->s1_3; +s1_1->s1_3->final; +*/ + +obj?.aaa.bbb(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="Identifier (aaa)\nMemberExpression:exit\nIdentifier (bbb)\nMemberExpression:exit\nIdentifier (arg)\nCallExpression:exit"]; + s1_3[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3; + s1_1->s1_3->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--simple-2.js b/tests/fixtures/code-path-analysis/optional-chaining--simple-2.js new file mode 100644 index 00000000000..a5867528678 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--simple-2.js @@ -0,0 +1,19 @@ +/*expected +initial->s1_1->s1_2->s1_3; +s1_1->s1_3->final; +*/ + +obj.foo?.(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nIdentifier (obj)\nIdentifier (foo)\nMemberExpression:exit"]; + s1_2[label="Identifier (arg)\nCallExpression:exit"]; + s1_3[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3; + s1_1->s1_3->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--simple-3.js b/tests/fixtures/code-path-analysis/optional-chaining--simple-3.js new file mode 100644 index 00000000000..fa43af967ed --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--simple-3.js @@ -0,0 +1,25 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_4->s1_7; +s1_1->s1_7; +s1_2->s1_7; +s1_3->s1_7->final; +*/ + +obj?.aaa?.bbb?.(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="Identifier (aaa)\nMemberExpression:exit"]; + s1_3[label="Identifier (bbb)\nMemberExpression:exit"]; + s1_4[label="Identifier (arg)\nCallExpression:exit"]; + s1_7[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3->s1_4->s1_7; + s1_1->s1_7; + s1_2->s1_7; + s1_3->s1_7->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--simple-4.js b/tests/fixtures/code-path-analysis/optional-chaining--simple-4.js new file mode 100644 index 00000000000..c10174646bd --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--simple-4.js @@ -0,0 +1,19 @@ +/*expected +initial->s1_1->s1_2->s1_3; +s1_1->s1_3->final; +*/ + +func?.()(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nCallExpression:enter\nIdentifier (func)\nCallExpression:exit"]; + s1_2[label="Identifier (arg)\nCallExpression:exit"]; + s1_3[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3; + s1_1->s1_3->final; +} +*/ diff --git a/tests/fixtures/config-file/cloned-config/circularRefEslintConfig.js b/tests/fixtures/config-file/cloned-config/circularRefEslintConfig.js deleted file mode 100644 index cc9fcdbcc7c..00000000000 --- a/tests/fixtures/config-file/cloned-config/circularRefEslintConfig.js +++ /dev/null @@ -1,15 +0,0 @@ -let errorConfig = ["error", {}]; - -class CircularRef { - constructor() { - this.self = this; - } -} - -module.exports = { - settings: { - sharedData: new CircularRef() - }, - - rules: { camelcase: errorConfig, "default-case": errorConfig } -}; diff --git a/tests/fixtures/config-file/cloned-config/eslintConfig.js b/tests/fixtures/config-file/cloned-config/eslintConfig.js deleted file mode 100644 index 8602e60b0fd..00000000000 --- a/tests/fixtures/config-file/cloned-config/eslintConfig.js +++ /dev/null @@ -1,4 +0,0 @@ -let errorConfig = ["error", {}]; -module.exports = { - rules: { camelcase: errorConfig, "default-case": errorConfig } -}; diff --git a/tests/fixtures/config-file/cloned-config/eslintConfigFail.js b/tests/fixtures/config-file/cloned-config/eslintConfigFail.js deleted file mode 100644 index b8702c6ffb8..00000000000 --- a/tests/fixtures/config-file/cloned-config/eslintConfigFail.js +++ /dev/null @@ -1,13 +0,0 @@ -let errorConfig = ["error", {}]; -module.exports = { - rules: { - camelcase: errorConfig, - "default-case": errorConfig , - "camelcase" : [ - 'error', - { - "ignoreDestructuring": new Date().getUTCFullYear // returns a function - } - ] - } -}; diff --git a/tests/fixtures/config-file/cloned-config/index.js b/tests/fixtures/config-file/cloned-config/index.js deleted file mode 100644 index b792f78cad3..00000000000 --- a/tests/fixtures/config-file/cloned-config/index.js +++ /dev/null @@ -1 +0,0 @@ -var someValue = "bar"; diff --git a/tests/fixtures/config-file/cloned-config/inlineText.js b/tests/fixtures/config-file/cloned-config/inlineText.js deleted file mode 100644 index e000713abaf..00000000000 --- a/tests/fixtures/config-file/cloned-config/inlineText.js +++ /dev/null @@ -1,3 +0,0 @@ -/*eslint default-case: ["error", {}]*/ -/*eslint camelcase: ["error", {}]*/ -var someValue = "bar"; diff --git a/tests/fixtures/parsers/arrow-parens/generics-extends-complex.js b/tests/fixtures/parsers/arrow-parens/generics-extends-complex.js new file mode 100644 index 00000000000..4a27b495277 --- /dev/null +++ b/tests/fixtures/parsers/arrow-parens/generics-extends-complex.js @@ -0,0 +1,249 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser@3.5.0 + * Source code: + * (a) => b + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [ + { + type: "Identifier", + name: "a", + range: [24, 25], + loc: { + start: { line: 1, column: 24 }, + end: { line: 1, column: 25 }, + }, + }, + ], + body: { + type: "Identifier", + name: "b", + range: [30, 31], + loc: { start: { line: 1, column: 30 }, end: { line: 1, column: 31 } }, + }, + async: false, + expression: true, + range: [0, 31], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 31 } }, + typeParameters: { + type: "TSTypeParameterDeclaration", + range: [0, 23], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 23 } }, + params: [ + { + type: "TSTypeParameter", + name: { + type: "Identifier", + name: "T", + range: [1, 2], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 2 }, + }, + }, + constraint: { + type: "TSIntersectionType", + types: [ + { + type: "TSParenthesizedType", + typeAnnotation: { + type: "TSUnionType", + types: [ + { + type: "TSTypeReference", + typeName: { + type: "Identifier", + name: "A", + range: [12, 13], + loc: { + start: { line: 1, column: 12 }, + end: { line: 1, column: 13 }, + }, + }, + range: [12, 13], + loc: { + start: { line: 1, column: 12 }, + end: { line: 1, column: 13 }, + }, + }, + { + type: "TSTypeReference", + typeName: { + type: "Identifier", + name: "B", + range: [16, 17], + loc: { + start: { line: 1, column: 16 }, + end: { line: 1, column: 17 }, + }, + }, + range: [16, 17], + loc: { + start: { line: 1, column: 16 }, + end: { line: 1, column: 17 }, + }, + }, + ], + range: [12, 17], + loc: { + start: { line: 1, column: 12 }, + end: { line: 1, column: 17 }, + }, + }, + range: [11, 18], + loc: { + start: { line: 1, column: 11 }, + end: { line: 1, column: 18 }, + }, + }, + { + type: "TSTypeReference", + typeName: { + type: "Identifier", + name: "C", + range: [21, 22], + loc: { + start: { line: 1, column: 21 }, + end: { line: 1, column: 22 }, + }, + }, + range: [21, 22], + loc: { + start: { line: 1, column: 21 }, + end: { line: 1, column: 22 }, + }, + }, + ], + range: [11, 22], + loc: { + start: { line: 1, column: 11 }, + end: { line: 1, column: 22 }, + }, + }, + range: [1, 22], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 22 }, + }, + }, + ], + }, + }, + range: [0, 31], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 31 } }, + }, + ], + sourceType: "script", + range: [0, 31], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 31 } }, + tokens: [ + { + type: "Punctuator", + value: "<", + range: [0, 1], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }, + }, + { + type: "Identifier", + value: "T", + range: [1, 2], + loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, + }, + { + type: "Keyword", + value: "extends", + range: [3, 10], + loc: { start: { line: 1, column: 3 }, end: { line: 1, column: 10 } }, + }, + { + type: "Punctuator", + value: "(", + range: [11, 12], + loc: { start: { line: 1, column: 11 }, end: { line: 1, column: 12 } }, + }, + { + type: "Identifier", + value: "A", + range: [12, 13], + loc: { start: { line: 1, column: 12 }, end: { line: 1, column: 13 } }, + }, + { + type: "Punctuator", + value: "|", + range: [14, 15], + loc: { start: { line: 1, column: 14 }, end: { line: 1, column: 15 } }, + }, + { + type: "Identifier", + value: "B", + range: [16, 17], + loc: { start: { line: 1, column: 16 }, end: { line: 1, column: 17 } }, + }, + { + type: "Punctuator", + value: ")", + range: [17, 18], + loc: { start: { line: 1, column: 17 }, end: { line: 1, column: 18 } }, + }, + { + type: "Punctuator", + value: "&", + range: [19, 20], + loc: { start: { line: 1, column: 19 }, end: { line: 1, column: 20 } }, + }, + { + type: "Identifier", + value: "C", + range: [21, 22], + loc: { start: { line: 1, column: 21 }, end: { line: 1, column: 22 } }, + }, + { + type: "Punctuator", + value: ">", + range: [22, 23], + loc: { start: { line: 1, column: 22 }, end: { line: 1, column: 23 } }, + }, + { + type: "Punctuator", + value: "(", + range: [23, 24], + loc: { start: { line: 1, column: 23 }, end: { line: 1, column: 24 } }, + }, + { + type: "Identifier", + value: "a", + range: [24, 25], + loc: { start: { line: 1, column: 24 }, end: { line: 1, column: 25 } }, + }, + { + type: "Punctuator", + value: ")", + range: [25, 26], + loc: { start: { line: 1, column: 25 }, end: { line: 1, column: 26 } }, + }, + { + type: "Punctuator", + value: "=>", + range: [27, 29], + loc: { start: { line: 1, column: 27 }, end: { line: 1, column: 29 } }, + }, + { + type: "Identifier", + value: "b", + range: [30, 31], + loc: { start: { line: 1, column: 30 }, end: { line: 1, column: 31 } }, + }, + ], + comments: [], + }); diff --git a/tests/fixtures/parsers/arrow-parens/generics-extends.js b/tests/fixtures/parsers/arrow-parens/generics-extends.js new file mode 100644 index 00000000000..ee70b565635 --- /dev/null +++ b/tests/fixtures/parsers/arrow-parens/generics-extends.js @@ -0,0 +1,151 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser@3.5.0 + * Source code: + * (a) => b + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [ + { + type: "Identifier", + name: "a", + range: [14, 15], + loc: { + start: { line: 1, column: 14 }, + end: { line: 1, column: 15 }, + }, + }, + ], + body: { + type: "Identifier", + name: "b", + range: [20, 21], + loc: { start: { line: 1, column: 20 }, end: { line: 1, column: 21 } }, + }, + async: false, + expression: true, + range: [0, 21], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 21 } }, + typeParameters: { + type: "TSTypeParameterDeclaration", + range: [0, 13], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 13 } }, + params: [ + { + type: "TSTypeParameter", + name: { + type: "Identifier", + name: "T", + range: [1, 2], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 2 }, + }, + }, + constraint: { + type: "TSTypeReference", + typeName: { + type: "Identifier", + name: "A", + range: [11, 12], + loc: { + start: { line: 1, column: 11 }, + end: { line: 1, column: 12 }, + }, + }, + range: [11, 12], + loc: { + start: { line: 1, column: 11 }, + end: { line: 1, column: 12 }, + }, + }, + range: [1, 12], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 12 }, + }, + }, + ], + }, + }, + range: [0, 21], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 21 } }, + }, + ], + sourceType: "script", + range: [0, 21], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 21 } }, + tokens: [ + { + type: "Punctuator", + value: "<", + range: [0, 1], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }, + }, + { + type: "Identifier", + value: "T", + range: [1, 2], + loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, + }, + { + type: "Keyword", + value: "extends", + range: [3, 10], + loc: { start: { line: 1, column: 3 }, end: { line: 1, column: 10 } }, + }, + { + type: "Identifier", + value: "A", + range: [11, 12], + loc: { start: { line: 1, column: 11 }, end: { line: 1, column: 12 } }, + }, + { + type: "Punctuator", + value: ">", + range: [12, 13], + loc: { start: { line: 1, column: 12 }, end: { line: 1, column: 13 } }, + }, + { + type: "Punctuator", + value: "(", + range: [13, 14], + loc: { start: { line: 1, column: 13 }, end: { line: 1, column: 14 } }, + }, + { + type: "Identifier", + value: "a", + range: [14, 15], + loc: { start: { line: 1, column: 14 }, end: { line: 1, column: 15 } }, + }, + { + type: "Punctuator", + value: ")", + range: [15, 16], + loc: { start: { line: 1, column: 15 }, end: { line: 1, column: 16 } }, + }, + { + type: "Punctuator", + value: "=>", + range: [17, 19], + loc: { start: { line: 1, column: 17 }, end: { line: 1, column: 19 } }, + }, + { + type: "Identifier", + value: "b", + range: [20, 21], + loc: { start: { line: 1, column: 20 }, end: { line: 1, column: 21 } }, + }, + ], + comments: [], + }); diff --git a/tests/fixtures/parsers/arrow-parens/generics-simple-async.js b/tests/fixtures/parsers/arrow-parens/generics-simple-async.js new file mode 100644 index 00000000000..68926127f2b --- /dev/null +++ b/tests/fixtures/parsers/arrow-parens/generics-simple-async.js @@ -0,0 +1,128 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser@3.5.0 + * Source code: + * async (a) => b + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [ + { + type: "Identifier", + name: "a", + range: [10, 11], + loc: { + start: { line: 1, column: 10 }, + end: { line: 1, column: 11 }, + }, + }, + ], + body: { + type: "Identifier", + name: "b", + range: [16, 17], + loc: { start: { line: 1, column: 16 }, end: { line: 1, column: 17 } }, + }, + async: true, + expression: true, + range: [0, 17], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 17 } }, + typeParameters: { + type: "TSTypeParameterDeclaration", + range: [6, 9], + loc: { start: { line: 1, column: 6 }, end: { line: 1, column: 9 } }, + params: [ + { + type: "TSTypeParameter", + name: { + type: "Identifier", + name: "T", + range: [7, 8], + loc: { + start: { line: 1, column: 7 }, + end: { line: 1, column: 8 }, + }, + }, + range: [7, 8], + loc: { + start: { line: 1, column: 7 }, + end: { line: 1, column: 8 }, + }, + }, + ], + }, + }, + range: [0, 17], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 17 } }, + }, + ], + sourceType: "script", + range: [0, 17], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 17 } }, + tokens: [ + { + type: "Identifier", + value: "async", + range: [0, 5], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 5 } }, + }, + { + type: "Punctuator", + value: "<", + range: [6, 7], + loc: { start: { line: 1, column: 6 }, end: { line: 1, column: 7 } }, + }, + { + type: "Identifier", + value: "T", + range: [7, 8], + loc: { start: { line: 1, column: 7 }, end: { line: 1, column: 8 } }, + }, + { + type: "Punctuator", + value: ">", + range: [8, 9], + loc: { start: { line: 1, column: 8 }, end: { line: 1, column: 9 } }, + }, + { + type: "Punctuator", + value: "(", + range: [9, 10], + loc: { start: { line: 1, column: 9 }, end: { line: 1, column: 10 } }, + }, + { + type: "Identifier", + value: "a", + range: [10, 11], + loc: { start: { line: 1, column: 10 }, end: { line: 1, column: 11 } }, + }, + { + type: "Punctuator", + value: ")", + range: [11, 12], + loc: { start: { line: 1, column: 11 }, end: { line: 1, column: 12 } }, + }, + { + type: "Punctuator", + value: "=>", + range: [13, 15], + loc: { start: { line: 1, column: 13 }, end: { line: 1, column: 15 } }, + }, + { + type: "Identifier", + value: "b", + range: [16, 17], + loc: { start: { line: 1, column: 16 }, end: { line: 1, column: 17 } }, + }, + ], + comments: [], + }); diff --git a/tests/fixtures/parsers/arrow-parens/generics-simple-no-params.js b/tests/fixtures/parsers/arrow-parens/generics-simple-no-params.js new file mode 100644 index 00000000000..ab17a7d8804 --- /dev/null +++ b/tests/fixtures/parsers/arrow-parens/generics-simple-no-params.js @@ -0,0 +1,106 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser@3.5.0 + * Source code: + * () => b + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [], + body: { + type: "Identifier", + name: "b", + range: [9, 10], + loc: { start: { line: 1, column: 9 }, end: { line: 1, column: 10 } }, + }, + async: false, + expression: true, + range: [0, 10], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } }, + typeParameters: { + type: "TSTypeParameterDeclaration", + range: [0, 3], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 3 } }, + params: [ + { + type: "TSTypeParameter", + name: { + type: "Identifier", + name: "T", + range: [1, 2], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 2 }, + }, + }, + range: [1, 2], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 2 }, + }, + }, + ], + }, + }, + range: [0, 10], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } }, + }, + ], + sourceType: "script", + range: [0, 10], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } }, + tokens: [ + { + type: "Punctuator", + value: "<", + range: [0, 1], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }, + }, + { + type: "Identifier", + value: "T", + range: [1, 2], + loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, + }, + { + type: "Punctuator", + value: ">", + range: [2, 3], + loc: { start: { line: 1, column: 2 }, end: { line: 1, column: 3 } }, + }, + { + type: "Punctuator", + value: "(", + range: [3, 4], + loc: { start: { line: 1, column: 3 }, end: { line: 1, column: 4 } }, + }, + { + type: "Punctuator", + value: ")", + range: [4, 5], + loc: { start: { line: 1, column: 4 }, end: { line: 1, column: 5 } }, + }, + { + type: "Punctuator", + value: "=>", + range: [6, 8], + loc: { start: { line: 1, column: 6 }, end: { line: 1, column: 8 } }, + }, + { + type: "Identifier", + value: "b", + range: [9, 10], + loc: { start: { line: 1, column: 9 }, end: { line: 1, column: 10 } }, + }, + ], + comments: [], + }); diff --git a/tests/fixtures/parsers/arrow-parens/generics-simple.js b/tests/fixtures/parsers/arrow-parens/generics-simple.js new file mode 100644 index 00000000000..92f1a5bdb81 --- /dev/null +++ b/tests/fixtures/parsers/arrow-parens/generics-simple.js @@ -0,0 +1,119 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser@3.5.0 + * Source code: + * (a) => b + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [ + { + type: "Identifier", + name: "a", + range: [4, 5], + loc: { start: { line: 1, column: 4 }, end: { line: 1, column: 5 } }, + }, + ], + body: { + type: "Identifier", + name: "b", + range: [10, 11], + loc: { start: { line: 1, column: 10 }, end: { line: 1, column: 11 } }, + }, + async: false, + expression: true, + range: [0, 11], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 11 } }, + typeParameters: { + type: "TSTypeParameterDeclaration", + range: [0, 3], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 3 } }, + params: [ + { + type: "TSTypeParameter", + name: { + type: "Identifier", + name: "T", + range: [1, 2], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 2 }, + }, + }, + range: [1, 2], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 2 }, + }, + }, + ], + }, + }, + range: [0, 11], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 11 } }, + }, + ], + sourceType: "script", + range: [0, 11], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 11 } }, + tokens: [ + { + type: "Punctuator", + value: "<", + range: [0, 1], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }, + }, + { + type: "Identifier", + value: "T", + range: [1, 2], + loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, + }, + { + type: "Punctuator", + value: ">", + range: [2, 3], + loc: { start: { line: 1, column: 2 }, end: { line: 1, column: 3 } }, + }, + { + type: "Punctuator", + value: "(", + range: [3, 4], + loc: { start: { line: 1, column: 3 }, end: { line: 1, column: 4 } }, + }, + { + type: "Identifier", + value: "a", + range: [4, 5], + loc: { start: { line: 1, column: 4 }, end: { line: 1, column: 5 } }, + }, + { + type: "Punctuator", + value: ")", + range: [5, 6], + loc: { start: { line: 1, column: 5 }, end: { line: 1, column: 6 } }, + }, + { + type: "Punctuator", + value: "=>", + range: [7, 9], + loc: { start: { line: 1, column: 7 }, end: { line: 1, column: 9 } }, + }, + { + type: "Identifier", + value: "b", + range: [10, 11], + loc: { start: { line: 1, column: 10 }, end: { line: 1, column: 11 } }, + }, + ], + comments: [], + }); diff --git a/tests/fixtures/testers/rule-tester/no-var.js b/tests/fixtures/testers/rule-tester/no-var.js index 382885613d2..5841f15bfa1 100644 --- a/tests/fixtures/testers/rule-tester/no-var.js +++ b/tests/fixtures/testers/rule-tester/no-var.js @@ -7,27 +7,31 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - - "use strict"; - - - var sourceCode = context.getSourceCode(); - - return { - - "VariableDeclaration": function(node) { - if (node.kind === "var") { - context.report({ - node: node, - loc: sourceCode.getFirstToken(node).loc, - message: "Bad var.", - fix: function(fixer) { - return fixer.remove(sourceCode.getFirstToken(node)); - } - }) +"use strict"; + +module.exports = { + + meta: { + fixable: "code" + }, + + create(context) { + + var sourceCode = context.getSourceCode(); + + return { + "VariableDeclaration": function(node) { + if (node.kind === "var") { + context.report({ + node: node, + loc: sourceCode.getFirstToken(node).loc, + message: "Bad var.", + fix: function(fixer) { + return fixer.remove(sourceCode.getFirstToken(node)); + } + }) + } } - } - }; - + }; + } }; diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index fb3c365bbbf..0ba01d2a5df 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -12,7 +12,6 @@ const assert = require("chai").assert, path = require("path"), sinon = require("sinon"), - leche = require("leche"), shell = require("shelljs"), fs = require("fs"), os = require("os"), @@ -4469,7 +4468,9 @@ describe("CLIEngine", () => { }); it("should call fs.writeFileSync() for each result with output", () => { - const fakeFS = leche.fake(fs), + const fakeFS = { + writeFileSync: () => {} + }, localCLIEngine = proxyquire("../../../lib/cli-engine/cli-engine", { fs: fakeFS }).CLIEngine, @@ -4486,7 +4487,6 @@ describe("CLIEngine", () => { ] }; - fakeFS.writeFileSync = function() {}; const spy = sinon.spy(fakeFS, "writeFileSync"); localCLIEngine.outputFixes(report); @@ -4498,7 +4498,9 @@ describe("CLIEngine", () => { }); it("should call fs.writeFileSync() for each result with output and not at all for a result without output", () => { - const fakeFS = leche.fake(fs), + const fakeFS = { + writeFileSync: () => {} + }, localCLIEngine = proxyquire("../../../lib/cli-engine/cli-engine", { fs: fakeFS }).CLIEngine, @@ -4518,7 +4520,6 @@ describe("CLIEngine", () => { ] }; - fakeFS.writeFileSync = function() {}; const spy = sinon.spy(fakeFS, "writeFileSync"); localCLIEngine.outputFixes(report); @@ -4555,12 +4556,12 @@ describe("CLIEngine", () => { describe("resolveFileGlobPatterns", () => { - leche.withData([ + [ [".", ["**/*.{js}"]], ["./", ["**/*.{js}"]], ["../", ["../**/*.{js}"]], ["", []] - ], (input, expected) => { + ].forEach(([input, expected]) => { it(`should correctly resolve ${input} to ${expected}`, () => { const engine = new CLIEngine(); diff --git a/tests/lib/cli-engine/formatters/checkstyle.js b/tests/lib/cli-engine/formatters/checkstyle.js index 2a72da15ece..218b15310ab 100644 --- a/tests/lib/cli-engine/formatters/checkstyle.js +++ b/tests/lib/cli-engine/formatters/checkstyle.js @@ -151,4 +151,21 @@ describe("formatter:checkstyle", () => { assert.strictEqual(result, ""); }); }); + + describe("when passing single message without line and column", () => { + const code = [{ + filePath: "foo.js", + messages: [{ + message: "Unexpected foo.", + severity: 2, + ruleId: "foo" + }] + }]; + + it("should return line and column as 0 instead of undefined", () => { + const result = formatter(code); + + assert.strictEqual(result, ""); + }); + }); }); diff --git a/tests/lib/cli.js b/tests/lib/cli.js index afb225f1f7c..a1d9a23e491 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -187,7 +187,6 @@ describe("cli", () => { }); }); - describe("when given a config with environment set to Node.js", () => { it("should execute without any errors", async () => { const configPath = getFixturePath("configurations", "env-node.json"); @@ -1161,58 +1160,4 @@ describe("cli", () => { }); }); - describe("testing the cloned config", () => { - describe("config file and input file", () => { - it("should not modify original configuration object", async () => { - const configPath = getFixturePath("config-file", "cloned-config", "eslintConfig.js"); - const filePath = getFixturePath("config-file", "cloned-config", "index.js"); - const args = `--config ${configPath} ${filePath}`; - - const exit = await cli.execute(args); - - assert.strictEqual(exit, 0); - }); - }); - - describe("config file and input file", () => { - it("should exit with 1 as camelcase has wrong property type", async () => { - const configPath = getFixturePath("config-file", "cloned-config", "eslintConfigFail.js"); - const filePath = getFixturePath("config-file", "cloned-config", "index.js"); - const args = `--config ${configPath} ${filePath}`; - - try { - await cli.execute(args); - } catch (error) { - assert.strictEqual(/Configuration for rule "camelcase" is invalid:/u.test(error), true); - } - - }); - }); - - describe("inline config and input file", () => { - it("should not modify original configuration object", async () => { - const filePath = getFixturePath("config-file", "cloned-config", "inlineText.js"); - const args = `${filePath}`; - - const exit = await cli.execute(args); - - assert.strictEqual(exit, 0); - }); - }); - - }); - - describe("handling circular reference while cloning", () => { - it("should handle circular ref", async () => { - const configPath = getFixturePath("config-file", "cloned-config", "circularRefEslintConfig.js"); - const filePath = getFixturePath("config-file", "cloned-config", "index.js"); - const args = `--config ${configPath} ${filePath}`; - - try { - await cli.execute(args); - } catch (error) { - assert.instanceOf(error, Error); - } - }); - }); }); diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 6807a57aa8a..d29dbb01640 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -16,7 +16,6 @@ const os = require("os"); const path = require("path"); const escapeStringRegExp = require("escape-string-regexp"); const fCache = require("file-entry-cache"); -const leche = require("leche"); const sinon = require("sinon"); const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); const shell = require("shelljs"); @@ -4427,8 +4426,10 @@ describe("ESLint", () => { }); it("should call fs.writeFile() for each result with output", async () => { - const fakeFS = leche.fake(fs); - const spy = fakeFS.writeFile = sinon.spy(callLastArgument); + const fakeFS = { + writeFile: sinon.spy(callLastArgument) + }; + const spy = fakeFS.writeFile; const localESLint = proxyquire("../../../lib/eslint/eslint", { fs: fakeFS }).ESLint; @@ -4451,8 +4452,10 @@ describe("ESLint", () => { }); it("should call fs.writeFile() for each result with output and not at all for a result without output", async () => { - const fakeFS = leche.fake(fs); - const spy = fakeFS.writeFile = sinon.spy(callLastArgument); + const fakeFS = { + writeFile: sinon.spy(callLastArgument) + }; + const spy = fakeFS.writeFile; const localESLint = proxyquire("../../../lib/eslint/eslint", { fs: fakeFS }).ESLint; diff --git a/tests/lib/init/config-file.js b/tests/lib/init/config-file.js index 69556b4b5b6..e9fe62e2c30 100644 --- a/tests/lib/init/config-file.js +++ b/tests/lib/init/config-file.js @@ -9,10 +9,8 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, - leche = require("leche"), sinon = require("sinon"), path = require("path"), - fs = require("fs"), yaml = require("js-yaml"), espree = require("espree"), ConfigFile = require("../../../lib/init/config-file"), @@ -59,15 +57,17 @@ describe("ConfigFile", () => { sinon.verifyAndRestore(); }); - leche.withData([ + [ ["JavaScript", "foo.js", espree.parse], ["JSON", "bar.json", JSON.parse], ["YAML", "foo.yaml", yaml.safeLoad], ["YML", "foo.yml", yaml.safeLoad] - ], (fileType, filename, validate) => { + ].forEach(([fileType, filename, validate]) => { it(`should write a file through fs when a ${fileType} path is passed`, () => { - const fakeFS = leche.fake(fs); + const fakeFS = { + writeFileSync: () => {} + }; sinon.mock(fakeFS).expects("writeFileSync").withExactArgs( filename, @@ -83,7 +83,9 @@ describe("ConfigFile", () => { }); it("should include a newline character at EOF", () => { - const fakeFS = leche.fake(fs); + const fakeFS = { + writeFileSync: () => {} + }; sinon.mock(fakeFS).expects("writeFileSync").withExactArgs( filename, @@ -100,7 +102,9 @@ describe("ConfigFile", () => { }); it("should make sure js config files match linting rules", () => { - const fakeFS = leche.fake(fs); + const fakeFS = { + writeFileSync: () => {} + }; const singleQuoteConfig = { rules: { @@ -122,7 +126,9 @@ describe("ConfigFile", () => { }); it("should still write a js config file even if linting fails", () => { - const fakeFS = leche.fake(fs); + const fakeFS = { + writeFileSync: () => {} + }; const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ baseConfig: config, fix: true, diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 7e26e1a826c..3f2393621b6 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -304,6 +304,10 @@ describe("RuleTester", () => { it("should use strict equality to compare output", () => { const replaceProgramWith5Rule = { + meta: { + fixable: "code" + }, + create: context => ({ Program(node) { context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); @@ -1237,6 +1241,73 @@ describe("RuleTester", () => { }, "Error must specify 'messageId' if 'data' is used."); }); + // fixable rules with or without `meta` property + it("should not throw an error if a rule that has `meta.fixable` produces fixes", () => { + const replaceProgramWith5Rule = { + meta: { + fixable: "code" + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); + } + }; + } + }; + + ruleTester.run("replaceProgramWith5", replaceProgramWith5Rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", output: "5", errors: 1 } + ] + }); + }); + it("should throw an error if a new-format rule that doesn't have `meta` produces fixes", () => { + const replaceProgramWith5Rule = { + create(context) { + return { + Program(node) { + context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("replaceProgramWith5", replaceProgramWith5Rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", output: "5", errors: 1 } + ] + }); + }, "Fixable rules should export a `meta.fixable` property."); + }); + it("should throw an error if a legacy-format rule produces fixes", () => { + + /** + * Legacy-format rule (a function instead of an object with `create` method). + * @param {RuleContext} context The ESLint rule context object. + * @returns {Object} Listeners. + */ + function replaceProgramWith5Rule(context) { + return { + Program(node) { + context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); + } + }; + } + + assert.throws(() => { + ruleTester.run("replaceProgramWith5", replaceProgramWith5Rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", output: "5", errors: 1 } + ] + }); + }, "Fixable rules should export a `meta.fixable` property."); + }); + describe("suggestions", () => { it("should pass with valid suggestions (tested using desc)", () => { ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { diff --git a/tests/lib/rules/accessor-pairs.js b/tests/lib/rules/accessor-pairs.js index e8f143ada4c..c6341326647 100644 --- a/tests/lib/rules/accessor-pairs.js +++ b/tests/lib/rules/accessor-pairs.js @@ -1087,6 +1087,46 @@ ruleTester.run("accessor-pairs", rule, { code: "Object.create(null, {foo: {set: function(value) {}}});", errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] }, + { + code: "var o = {d: 1};\n Object?.defineProperty(o, 'c', \n{set: function(value) {\n val = value; \n} \n});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "Reflect?.defineProperty(obj, 'foo', {set: function(value) {}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "Object?.defineProperties(obj, {foo: {set: function(value) {}}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "Object?.create(null, {foo: {set: function(value) {}}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "var o = {d: 1};\n (Object?.defineProperty)(o, 'c', \n{set: function(value) {\n val = value; \n} \n});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "(Reflect?.defineProperty)(obj, 'foo', {set: function(value) {}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "(Object?.defineProperties)(obj, {foo: {set: function(value) {}}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "(Object?.create)(null, {foo: {set: function(value) {}}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, //------------------------------------------------------------------------------ // Classes diff --git a/tests/lib/rules/array-callback-return.js b/tests/lib/rules/array-callback-return.js index 24b40cb5204..6d6a4d86242 100644 --- a/tests/lib/rules/array-callback-return.js +++ b/tests/lib/rules/array-callback-return.js @@ -115,90 +115,91 @@ ruleTester.run("array-callback-return", rule, { ], invalid: [ - { code: "Array.from(x, function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "Array.from(x, function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "Int32Array.from(x, function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "Int32Array.from(x, function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.every(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.every(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.filter(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.filter(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.find(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.find(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.findIndex(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.findIndex(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.flatMap(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.flatMap(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.map(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.map(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.reduce(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.reduce(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.reduceRight(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.reduceRight(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.some(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.some(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.sort(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.sort(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.bar.baz.every(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.bar.baz.every(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo[\"every\"](function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo[\"every\"](function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo[`every`](function() {})", parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo[`every`](function foo() {})", parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.every(() => {})", parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Expected to return a value in arrow function.", column: 14 }] }, - { code: "foo.every(function() { if (a) return true; })", errors: [{ message: "Expected to return a value at the end of function.", column: 11 }] }, - { code: "foo.every(function cb() { if (a) return true; })", errors: [{ message: "Expected to return a value at the end of function 'cb'.", column: 11 }] }, - { code: "foo.every(function() { switch (a) { case 0: break; default: return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function" } }] }, - { code: "foo.every(function foo() { switch (a) { case 0: break; default: return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function 'foo'" } }] }, - { code: "foo.every(function() { try { bar(); } catch (err) { return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function" } }] }, - { code: "foo.every(function foo() { try { bar(); } catch (err) { return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function 'foo'" } }] }, - { code: "foo.every(function() { return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "Function" } }] }, - { code: "foo.every(function foo() { return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "Function 'foo'" } }] }, - { code: "foo.every(function() { if (a) return; })", errors: ["Expected to return a value at the end of function.", { messageId: "expectedReturnValue", data: { name: "Function" } }] }, - { code: "foo.every(function foo() { if (a) return; })", errors: ["Expected to return a value at the end of function 'foo'.", { messageId: "expectedReturnValue", data: { name: "Function 'foo'" } }] }, - { code: "foo.every(function() { if (a) return; else return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "Function" } }, { messageId: "expectedReturnValue", data: { name: "Function" } }] }, - { code: "foo.every(function foo() { if (a) return; else return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "Function 'foo'" } }, { messageId: "expectedReturnValue", data: { name: "Function 'foo'" } }] }, - { code: "foo.every(cb || function() {})", errors: ["Expected to return a value in function."] }, - { code: "foo.every(cb || function foo() {})", errors: ["Expected to return a value in function 'foo'."] }, - { code: "foo.every(a ? function() {} : function() {})", errors: ["Expected to return a value in function.", "Expected to return a value in function."] }, - { code: "foo.every(a ? function foo() {} : function bar() {})", errors: ["Expected to return a value in function 'foo'.", "Expected to return a value in function 'bar'."] }, - { code: "foo.every(function(){ return function() {}; }())", errors: [{ message: "Expected to return a value in function.", column: 30 }] }, - { code: "foo.every(function(){ return function foo() {}; }())", errors: [{ message: "Expected to return a value in function 'foo'.", column: 30 }] }, - { code: "foo.every(() => {})", options: [{ allowImplicit: false }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Expected to return a value in arrow function." }] }, - { code: "foo.every(() => {})", options: [{ allowImplicit: true }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Expected to return a value in arrow function." }] }, + { code: "Array.from(x, function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.from" } }] }, + { code: "Array.from(x, function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.from" } }] }, + { code: "Int32Array.from(x, function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.from" } }] }, + { code: "Int32Array.from(x, function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.from" } }] }, + { code: "foo.every(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.filter(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.filter" } }] }, + { code: "foo.filter(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, + { code: "foo.find(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.find" } }] }, + { code: "foo.find(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.find" } }] }, + { code: "foo.findIndex(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.findIndex" } }] }, + { code: "foo.findIndex(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.findIndex" } }] }, + { code: "foo.flatMap(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.flatMap" } }] }, + { code: "foo.flatMap(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.flatMap" } }] }, + { code: "foo.map(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.map" } }] }, + { code: "foo.map(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.map" } }] }, + { code: "foo.reduce(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.reduce" } }] }, + { code: "foo.reduce(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.reduce" } }] }, + { code: "foo.reduceRight(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.reduceRight" } }] }, + { code: "foo.reduceRight(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.reduceRight" } }] }, + { code: "foo.some(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.some" } }] }, + { code: "foo.some(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.some" } }] }, + { code: "foo.sort(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.sort" } }] }, + { code: "foo.sort(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.sort" } }] }, + { code: "foo.bar.baz.every(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.bar.baz.every(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo[\"every\"](function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo[\"every\"](function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo[`every`](function() {})", parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo[`every`](function foo() {})", parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(() => {})", parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Array.prototype.every() expects a return value from arrow function.", column: 14 }] }, + { code: "foo.every(function() { if (a) return true; })", errors: [{ message: "Array.prototype.every() expects a value to be returned at the end of function.", column: 11 }] }, + { code: "foo.every(function cb() { if (a) return true; })", errors: [{ message: "Array.prototype.every() expects a value to be returned at the end of function 'cb'.", column: 11 }] }, + { code: "foo.every(function() { switch (a) { case 0: break; default: return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function foo() { switch (a) { case 0: break; default: return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function() { try { bar(); } catch (err) { return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function foo() { try { bar(); } catch (err) { return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function() { return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function foo() { return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function() { if (a) return; })", errors: ["Array.prototype.every() expects a value to be returned at the end of function.", { messageId: "expectedReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function foo() { if (a) return; })", errors: ["Array.prototype.every() expects a value to be returned at the end of function 'foo'.", { messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function() { if (a) return; else return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.every" } }, { messageId: "expectedReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function foo() { if (a) return; else return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }, { messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(cb || function() {})", errors: ["Array.prototype.every() expects a return value from function."] }, + { code: "foo.every(cb || function foo() {})", errors: ["Array.prototype.every() expects a return value from function 'foo'."] }, + { code: "foo.every(a ? function() {} : function() {})", errors: ["Array.prototype.every() expects a return value from function.", "Array.prototype.every() expects a return value from function."] }, + { code: "foo.every(a ? function foo() {} : function bar() {})", errors: ["Array.prototype.every() expects a return value from function 'foo'.", "Array.prototype.every() expects a return value from function 'bar'."] }, + { code: "foo.every(function(){ return function() {}; }())", errors: [{ message: "Array.prototype.every() expects a return value from function.", column: 30 }] }, + { code: "foo.every(function(){ return function foo() {}; }())", errors: [{ message: "Array.prototype.every() expects a return value from function 'foo'.", column: 30 }] }, + { code: "foo.every(() => {})", options: [{ allowImplicit: false }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Array.prototype.every() expects a return value from arrow function." }] }, + { code: "foo.every(() => {})", options: [{ allowImplicit: true }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Array.prototype.every() expects a return value from arrow function." }] }, // options: { allowImplicit: true } - { code: "Array.from(x, function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.every(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.filter(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.find(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.map(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.reduce(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.reduceRight(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.bar.baz.every(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.every(cb || function() {})", options: allowImplicitOptions, errors: ["Expected to return a value in function."] }, - { code: "[\"foo\",\"bar\"].sort(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.forEach(x => x)", options: allowImplicitCheckForEach, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Arrow function" } }] }, - { code: "foo.forEach(function(x) { if (a == b) {return x;}})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function" } }] }, - { code: "foo.forEach(function bar(x) { return x;})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function 'bar'" } }] }, + { code: "Array.from(x, function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.from" } }] }, + { code: "foo.every(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.filter(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, + { code: "foo.find(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.find" } }] }, + { code: "foo.map(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.map" } }] }, + { code: "foo.reduce(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.reduce" } }] }, + { code: "foo.reduceRight(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.reduceRight" } }] }, + { code: "foo.bar.baz.every(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(cb || function() {})", options: allowImplicitOptions, errors: ["Array.prototype.every() expects a return value from function."] }, + { code: "[\"foo\",\"bar\"].sort(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.sort" } }] }, + { code: "foo.forEach(x => x)", options: allowImplicitCheckForEach, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function(x) { if (a == b) {return x;}})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function bar(x) { return x;})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function 'bar'", arrayMethodName: "Array.prototype.forEach" } }] }, // // options: { checkForEach: true } - { code: "foo.forEach(x => x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Arrow function" } }] }, - { code: "foo.forEach(val => y += val)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Arrow function" } }] }, - { code: "[\"foo\",\"bar\"].forEach(x => ++x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Arrow function" } }] }, - { code: "foo.bar().forEach(x => x === y)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Arrow function" } }] }, - { code: "foo.forEach(function() {return function() { if (a == b) { return a; }}}())", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function" } }] }, - { code: "foo.forEach(function(x) { if (a == b) {return x;}})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function" } }] }, - { code: "foo.forEach(function(x) { if (a == b) {return undefined;}})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function" } }] }, - { code: "foo.forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function 'bar'" } }] }, - { code: "foo.bar().forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function 'bar'" } }] }, - { code: "[\"foo\",\"bar\"].forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function 'bar'" } }] }, - { code: "foo.forEach((x) => { return x;})", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Arrow function" } }] }, - { code: "Array.from(x, function() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.every(function() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.filter(function foo() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.filter(function foo() { return; })", options: checkForEachOptions, errors: [{ messageId: "expectedReturnValue", data: { name: "Function 'foo'" } }] }, - { code: "foo.every(cb || function() {})", options: checkForEachOptions, errors: ["Expected to return a value in function."] }, + { code: "foo.forEach(x => x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(val => y += val)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "[\"foo\",\"bar\"].forEach(x => ++x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.bar().forEach(x => x === y)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function() {return function() { if (a == b) { return a; }}}())", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function(x) { if (a == b) {return x;}})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function(x) { if (a == b) {return undefined;}})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function 'bar'", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: ["Array.prototype.forEach() expects no useless return value from function 'bar'."] }, + { code: "foo.bar().forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function 'bar'", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "[\"foo\",\"bar\"].forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function 'bar'", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach((x) => { return x;})", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "Array.from(x, function() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.from" } }] }, + { code: "foo.every(function() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.filter(function foo() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, + { code: "foo.filter(function foo() { return; })", options: checkForEachOptions, errors: [{ messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, + { code: "foo.every(cb || function() {})", options: checkForEachOptions, errors: ["Array.prototype.every() expects a return value from function."] }, // full location tests { @@ -206,7 +207,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", - data: { name: "arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" }, type: "ArrowFunctionExpression", line: 1, column: 16, @@ -219,7 +220,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", - data: { name: "arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" }, type: "ArrowFunctionExpression", line: 2, column: 4, @@ -232,7 +233,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", - data: { name: "arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" }, type: "ArrowFunctionExpression", line: 1, column: 26, @@ -245,7 +246,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedReturnValue", - data: { name: "Arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" }, type: "ReturnStatement", line: 1, column: 21, @@ -258,7 +259,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", - data: { name: "arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.from" }, type: "ArrowFunctionExpression", line: 1, column: 21, @@ -272,7 +273,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", - data: { name: "Arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, type: "ArrowFunctionExpression", line: 1, column: 17, @@ -286,7 +287,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", - data: { name: "Arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, type: "ArrowFunctionExpression", line: 1, column: 41, @@ -300,7 +301,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", - data: { name: "Arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, type: "ArrowFunctionExpression", line: 2, column: 13, @@ -314,7 +315,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", - data: { name: "Arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, type: "ReturnStatement", line: 1, column: 52, @@ -326,7 +327,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function(){})", errors: [{ messageId: "expectedInside", - data: { name: "function" }, + data: { name: "function", arrayMethodName: "Array.prototype.filter" }, type: "FunctionExpression", line: 1, column: 12, @@ -338,7 +339,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function (){})", errors: [{ messageId: "expectedInside", - data: { name: "function" }, + data: { name: "function", arrayMethodName: "Array.prototype.filter" }, type: "FunctionExpression", line: 1, column: 12, @@ -350,7 +351,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function\n(){})", errors: [{ messageId: "expectedInside", - data: { name: "function" }, + data: { name: "function", arrayMethodName: "Array.prototype.filter" }, type: "FunctionExpression", line: 1, column: 12, @@ -362,7 +363,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function bar(){})", errors: [{ messageId: "expectedInside", - data: { name: "function 'bar'" }, + data: { name: "function 'bar'", arrayMethodName: "Array.prototype.filter" }, type: "FunctionExpression", line: 1, column: 12, @@ -374,7 +375,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function bar (){})", errors: [{ messageId: "expectedInside", - data: { name: "function 'bar'" }, + data: { name: "function 'bar'", arrayMethodName: "Array.prototype.filter" }, type: "FunctionExpression", line: 1, column: 12, @@ -386,7 +387,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function\n bar() {})", errors: [{ messageId: "expectedInside", - data: { name: "function 'bar'" }, + data: { name: "function 'bar'", arrayMethodName: "Array.prototype.filter" }, type: "FunctionExpression", line: 1, column: 12, @@ -398,7 +399,7 @@ ruleTester.run("array-callback-return", rule, { code: "Array.from(foo, function bar(){})", errors: [{ messageId: "expectedInside", - data: { name: "function 'bar'" }, + data: { name: "function 'bar'", arrayMethodName: "Array.from" }, type: "FunctionExpression", line: 1, column: 17, @@ -410,7 +411,7 @@ ruleTester.run("array-callback-return", rule, { code: "Array.from(foo, bar ? function (){} : baz)", errors: [{ messageId: "expectedInside", - data: { name: "function" }, + data: { name: "function", arrayMethodName: "Array.from" }, type: "FunctionExpression", line: 1, column: 23, @@ -422,7 +423,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function bar() { return \n })", errors: [{ messageId: "expectedReturnValue", - data: { name: "Function 'bar'" }, + data: { name: "function 'bar'", arrayMethodName: "Array.prototype.filter" }, type: "ReturnStatement", line: 1, column: 29, @@ -435,13 +436,40 @@ ruleTester.run("array-callback-return", rule, { options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", - data: { name: "Function" }, + data: { name: "function", arrayMethodName: "Array.prototype.forEach" }, type: "ReturnStatement", line: 2, column: 10, endLine: 2, endColumn: 20 }] + }, + + // Optional chaining + { + code: "foo?.filter(() => { console.log('hello') })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" } }] + }, + { + code: "(foo?.filter)(() => { console.log('hello') })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" } }] + }, + { + code: "Array?.from([], () => { console.log('hello') })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.from" } }] + }, + { + code: "(Array?.from)([], () => { console.log('hello') })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.from" } }] + }, + { + code: "foo?.filter((function() { return () => { console.log('hello') } })?.())", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" } }] } ] }); diff --git a/tests/lib/rules/arrow-body-style.js b/tests/lib/rules/arrow-body-style.js index 7c1bea64ef8..7a8de4fe5ef 100644 --- a/tests/lib/rules/arrow-body-style.js +++ b/tests/lib/rules/arrow-body-style.js @@ -45,6 +45,239 @@ ruleTester.run("arrow-body-style", rule, { { code: "var foo = () => { return { bar: 0 }; };", options: ["as-needed", { requireReturnForObjectLiteral: true }] } ], invalid: [ + { + code: "for (var foo = () => { return a in b ? bar : () => {} } ;;);", + output: "for (var foo = () => (a in b ? bar : () => {}) ;;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 22, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "a in b; for (var f = () => { return c };;);", + output: "a in b; for (var f = () => c;;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 28, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (a = b => { return c in d ? e : f } ;;);", + output: "for (a = b => (c in d ? e : f) ;;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 15, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (var f = () => { return a };;);", + output: "for (var f = () => a;;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 20, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (var f;f = () => { return a };);", + output: "for (var f;f = () => a;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 22, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (var f = () => { return a in c };;);", + output: "for (var f = () => (a in c);;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 20, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (var f;f = () => { return a in c };);", + output: "for (var f;f = () => a in c;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 22, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (;;){var f = () => { return a in c }}", + output: "for (;;){var f = () => a in c}", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 24, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (a = b => { return c = d in e } ;;);", + output: "for (a = b => (c = d in e) ;;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 15, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (var a;;a = b => { return c = d in e } );", + output: "for (var a;;a = b => c = d in e );", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 22, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (let a = (b, c, d) => { return vb && c in d; }; ;);", + output: "for (let a = (b, c, d) => (vb && c in d); ;);", + errors: [ + { + line: 1, + column: 27, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (let a = (b, c, d) => { return v in b && c in d; }; ;);", + output: "for (let a = (b, c, d) => (v in b && c in d); ;);", + errors: [ + { + line: 1, + column: 27, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "function foo(){ for (let a = (b, c, d) => { return v in b && c in d; }; ;); }", + output: "function foo(){ for (let a = (b, c, d) => (v in b && c in d); ;); }", + errors: [ + { + line: 1, + column: 43, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for ( a = (b, c, d) => { return v in b && c in d; }; ;);", + output: "for ( a = (b, c, d) => (v in b && c in d); ;);", + errors: [ + { + line: 1, + column: 24, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for ( a = (b) => { return (c in d) }; ;);", + output: "for ( a = (b) => (c in d); ;);", + errors: [ + { + line: 1, + column: 18, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (let a = (b, c, d) => { return vb in dd ; }; ;);", + output: "for (let a = (b, c, d) => (vb in dd ); ;);", + errors: [ + { + line: 1, + column: 27, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (let a = (b, c, d) => { return vb in c in dd ; }; ;);", + output: "for (let a = (b, c, d) => (vb in c in dd ); ;);", + errors: [ + { + line: 1, + column: 27, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "do{let a = () => {return f in ff}}while(true){}", + output: "do{let a = () => f in ff}while(true){}", + errors: [{ + line: 1, + column: 18, + messageId: "unexpectedSingleBlock" + }] + }, + { + code: "do{for (let a = (b, c, d) => { return vb in c in dd ; }; ;);}while(true){}", + output: "do{for (let a = (b, c, d) => (vb in c in dd ); ;);}while(true){}", + errors: [{ + line: 1, + column: 30, + messageId: "unexpectedSingleBlock" + }] + }, + { + code: "scores.map(score => { return x in +(score / maxScore).toFixed(2)});", + output: "scores.map(score => x in +(score / maxScore).toFixed(2));", + errors: [{ + line: 1, + column: 21, + messageId: "unexpectedSingleBlock" + }] + }, + { + code: "const fn = (a, b) => { return a + x in Number(b) };", + output: "const fn = (a, b) => a + x in Number(b);", + errors: [{ + line: 1, + column: 22, + messageId: "unexpectedSingleBlock" + }] + }, { code: "var foo = () => 0", output: "var foo = () => {return 0}", @@ -53,6 +286,8 @@ ruleTester.run("arrow-body-style", rule, { { line: 1, column: 17, + endLine: 1, + endColumn: 18, type: "ArrowFunctionExpression", messageId: "expectedBlock" } @@ -368,8 +603,8 @@ ruleTester.run("arrow-body-style", rule, { // Not fixed; fixing would cause ASI issues. code: - "var foo = () => { return bar }\n" + - "[1, 2, 3].map(foo)", + "var foo = () => { return bar }\n" + + "[1, 2, 3].map(foo)", output: null, options: ["never"], errors: [ @@ -378,10 +613,11 @@ ruleTester.run("arrow-body-style", rule, { }, { + // Not fixed; fixing would cause ASI issues. code: - "var foo = () => { return bar }\n" + - "(1).toString();", + "var foo = () => { return bar }\n" + + "(1).toString();", output: null, options: ["never"], errors: [ @@ -440,6 +676,8 @@ ruleTester.run("arrow-body-style", rule, { { line: 1, column: 17, + endLine: 3, + endColumn: 2, type: "ArrowFunctionExpression", messageId: "unexpectedSingleBlock" } @@ -452,6 +690,8 @@ ruleTester.run("arrow-body-style", rule, { { line: 1, column: 17, + endLine: 2, + endColumn: 13, type: "ArrowFunctionExpression", messageId: "unexpectedSingleBlock" } @@ -464,6 +704,8 @@ ruleTester.run("arrow-body-style", rule, { { line: 1, column: 17, + endLine: 2, + endColumn: 2, type: "ArrowFunctionExpression", messageId: "unexpectedSingleBlock" } @@ -508,6 +750,8 @@ ruleTester.run("arrow-body-style", rule, { { line: 2, column: 31, + endLine: 7, + endColumn: 16, type: "ArrowFunctionExpression", messageId: "unexpectedObjectBlock" } diff --git a/tests/lib/rules/arrow-parens.js b/tests/lib/rules/arrow-parens.js index edd1ae6da8a..61d76358440 100644 --- a/tests/lib/rules/arrow-parens.js +++ b/tests/lib/rules/arrow-parens.js @@ -45,16 +45,22 @@ const valid = [ { code: "a.then((foo) => {});", options: ["always"] }, { code: "a.then((foo) => { if (true) {}; });", options: ["always"] }, { code: "a.then(async (foo) => { if (true) {}; });", options: ["always"], parserOptions: { ecmaVersion: 8 } }, + { code: "(a: T) => a", options: ["always"], parser: parser("identifer-type") }, + { code: "(a): T => a", options: ["always"], parser: parser("return-type") }, // "as-needed" { code: "() => {}", options: ["as-needed"] }, { code: "a => {}", options: ["as-needed"] }, { code: "a => a", options: ["as-needed"] }, + { code: "a => (a)", options: ["as-needed"] }, + { code: "(a => a)", options: ["as-needed"] }, + { code: "((a => a))", options: ["as-needed"] }, { code: "([a, b]) => {}", options: ["as-needed"] }, { code: "({ a, b }) => {}", options: ["as-needed"] }, { code: "(a = 10) => {}", options: ["as-needed"] }, { code: "(...a) => a[0]", options: ["as-needed"] }, { code: "(a, b) => {}", options: ["as-needed"] }, + { code: "async a => a", options: ["as-needed"], parserOptions: { ecmaVersion: 8 } }, { code: "async ([a, b]) => {}", options: ["as-needed"], parserOptions: { ecmaVersion: 8 } }, { code: "async (a, b) => {}", options: ["as-needed"], parserOptions: { ecmaVersion: 8 } }, { code: "(a: T) => a", options: ["as-needed"], parser: parser("identifer-type") }, @@ -63,6 +69,9 @@ const valid = [ // "as-needed", { "requireForBlockBody": true } { code: "() => {}", options: ["as-needed", { requireForBlockBody: true }] }, { code: "a => a", options: ["as-needed", { requireForBlockBody: true }] }, + { code: "a => (a)", options: ["as-needed", { requireForBlockBody: true }] }, + { code: "(a => a)", options: ["as-needed", { requireForBlockBody: true }] }, + { code: "((a => a))", options: ["as-needed", { requireForBlockBody: true }] }, { code: "([a, b]) => {}", options: ["as-needed", { requireForBlockBody: true }] }, { code: "([a, b]) => a", options: ["as-needed", { requireForBlockBody: true }] }, { code: "({ a, b }) => {}", options: ["as-needed", { requireForBlockBody: true }] }, @@ -136,6 +145,83 @@ const valid = [ { code: "var bar = (/*comment here*/{a}) => a", options: ["as-needed"] + }, + + // generics + { + code: "(a) => b", + options: ["always"], + parser: parser("generics-simple") + }, + { + code: "(a) => b", + options: ["as-needed"], + parser: parser("generics-simple") + }, + { + code: "(a) => b", + options: ["as-needed", { requireForBlockBody: true }], + parser: parser("generics-simple") + }, + { + code: "async (a) => b", + options: ["always"], + parser: parser("generics-simple-async") + }, + { + code: "async (a) => b", + options: ["as-needed"], + parser: parser("generics-simple-async") + }, + { + code: "async (a) => b", + options: ["as-needed", { requireForBlockBody: true }], + parser: parser("generics-simple-async") + }, + { + code: "() => b", + options: ["always"], + parser: parser("generics-simple-no-params") + }, + { + code: "() => b", + options: ["as-needed"], + parser: parser("generics-simple-no-params") + }, + { + code: "() => b", + options: ["as-needed", { requireForBlockBody: true }], + parser: parser("generics-simple-no-params") + }, + { + code: "(a) => b", + options: ["always"], + parser: parser("generics-extends") + }, + { + code: "(a) => b", + options: ["as-needed"], + parser: parser("generics-extends") + }, + { + code: "(a) => b", + options: ["as-needed", { requireForBlockBody: true }], + parser: parser("generics-extends") + }, + { + code: "(a) => b", + options: ["always"], + parser: parser("generics-extends-complex") + }, + { + code: "(a) => b", + options: ["as-needed"], + parser: parser("generics-extends-complex") + }, + { + code: "(a) => b", + options: ["as-needed", { requireForBlockBody: true }], + parser: parser("generics-extends-complex") } ]; @@ -236,6 +322,30 @@ const invalid = [ type }] }, + { + code: "( a ) => b", + output: "a => b", + options: ["as-needed"], + errors: [{ + line: 1, + column: 4, + endColumn: 5, + messageId: "unexpectedParens", + type + }] + }, + { + code: "(\na\n) => b", + output: "a => b", + options: ["as-needed"], + errors: [{ + line: 2, + column: 1, + endColumn: 2, + messageId: "unexpectedParens", + type + }] + }, { code: "(a,) => a", output: "a => a", @@ -275,6 +385,30 @@ const invalid = [ type }] }, + { + code: "typeof((a) => {})", + output: "typeof(a => {})", + options: ["as-needed"], + errors: [{ + line: 1, + column: 9, + endColumn: 10, + messageId: "unexpectedParens", + type + }] + }, + { + code: "function *f() { yield(a) => a; }", + output: "function *f() { yield a => a; }", + options: ["as-needed"], + errors: [{ + line: 1, + column: 23, + endColumn: 24, + messageId: "unexpectedParens", + type + }] + }, // "as-needed", { "requireForBlockBody": true } { diff --git a/tests/lib/rules/camelcase.js b/tests/lib/rules/camelcase.js index 7c7a4576c77..4f9cdca78fe 100644 --- a/tests/lib/rules/camelcase.js +++ b/tests/lib/rules/camelcase.js @@ -169,6 +169,104 @@ ruleTester.run("camelcase", rule, { options: [{ ignoreImports: false }], parserOptions: { ecmaVersion: 6, sourceType: "module" } }, + { + code: "var _camelCased = aGlobalVariable", + options: [{ ignoreGlobals: false }], + globals: { aGlobalVariable: "readonly" } + }, + { + code: "var camelCased = _aGlobalVariable", + options: [{ ignoreGlobals: false }], + globals: { _aGlobalVariable: "readonly" } + }, + { + code: "var camelCased = a_global_variable", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable.foo()", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable[undefined]", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "var foo = a_global_variable.bar", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable.foo = bar", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "( { foo: a_global_variable.bar } = baz )", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable = foo", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable = foo", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "({ a_global_variable } = foo)", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "({ snake_cased: a_global_variable } = foo)", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "({ snake_cased: a_global_variable = foo } = bar)", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "[a_global_variable] = bar", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "[a_global_variable = foo] = bar", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "foo[a_global_variable] = bar", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "var foo = { [a_global_variable]: bar }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "var { [a_global_variable]: foo } = bar", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, { code: "function foo({ no_camelcased: camelCased }) {};", parserOptions: { ecmaVersion: 6 } @@ -652,6 +750,257 @@ ruleTester.run("camelcase", rule, { } ] }, + { + code: "var camelCased = snake_cased", + options: [{ ignoreGlobals: false }], + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "snake_cased" }, + type: "Identifier" + } + ] + }, + { + code: "a_global_variable.foo()", + options: [{ ignoreGlobals: false }], + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "a_global_variable[undefined]", + options: [{ ignoreGlobals: false }], + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "var camelCased = snake_cased", + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "snake_cased" }, + type: "Identifier" + } + ] + }, + { + code: "var camelCased = snake_cased", + options: [{}], + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "snake_cased" }, + type: "Identifier" + } + ] + }, + { + code: "foo.a_global_variable = bar", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { a_global_variable: bar }", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { a_global_variable: a_global_variable }", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 13 + } + ] + }, + { + code: "var foo = { a_global_variable() {} }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "class Foo { a_global_variable() {} }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "a_global_variable: for (;;);", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "if (foo) { let a_global_variable; a_global_variable = bar; }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 16 + }, + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 35 + } + ] + }, + { + code: "function foo(a_global_variable) { foo = a_global_variable; }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 14 + }, + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 41 + } + ] + }, + { + code: "var a_global_variable", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "function a_global_variable () {}", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "const a_global_variable = foo; bar = a_global_variable", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 7 + }, + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 38 + } + ] + }, + { + code: "bar = a_global_variable; var a_global_variable;", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 7 + }, + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 30 + } + ] + }, + { + code: "var foo = { a_global_variable }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, { code: "export * as snake_cased from 'mod'", parserOptions: { ecmaVersion: 2020, sourceType: "module" }, @@ -957,6 +1306,20 @@ ruleTester.run("camelcase", rule, { type: "Identifier" } ] + }, + + // Optional chaining. + { + code: "obj.o_k.non_camelcase = 0", + options: [{ properties: "always" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "notCamelCase", data: { name: "non_camelcase" } }] + }, + { + code: "(obj?.o_k).non_camelcase = 0", + options: [{ properties: "always" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "notCamelCase", data: { name: "non_camelcase" } }] } ] }); diff --git a/tests/lib/rules/computed-property-spacing.js b/tests/lib/rules/computed-property-spacing.js index b0ecef6eb81..a1e5834243e 100644 --- a/tests/lib/rules/computed-property-spacing.js +++ b/tests/lib/rules/computed-property-spacing.js @@ -1906,6 +1906,28 @@ ruleTester.run("computed-property-spacing", rule, { endColumn: 19 } ] + }, + + // Optional chaining + { + code: "obj?.[1];", + output: "obj?.[ 1 ];", + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "missingSpaceAfter", data: { tokenValue: "[" } }, + { messageId: "missingSpaceBefore", data: { tokenValue: "]" } } + ] + }, + { + code: "obj?.[ 1 ];", + output: "obj?.[1];", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "unexpectedSpaceAfter", data: { tokenValue: "[" } }, + { messageId: "unexpectedSpaceBefore", data: { tokenValue: "]" } } + ] } ] }); diff --git a/tests/lib/rules/constructor-super.js b/tests/lib/rules/constructor-super.js index b6c223dec7d..e20da576b5e 100644 --- a/tests/lib/rules/constructor-super.js +++ b/tests/lib/rules/constructor-super.js @@ -16,7 +16,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("constructor-super", rule, { valid: [ @@ -88,7 +88,10 @@ ruleTester.run("constructor-super", rule, { } } } - ` + `, + + // Optional chaining + "class A extends obj?.prop { constructor() { super(); } }" ], invalid: [ diff --git a/tests/lib/rules/dot-location.js b/tests/lib/rules/dot-location.js index 1a6ea37a733..e102bd6927d 100644 --- a/tests/lib/rules/dot-location.js +++ b/tests/lib/rules/dot-location.js @@ -136,6 +136,68 @@ ruleTester.run("dot-location", rule, { { code: "(\na &&\nb()\n).toString()", options: ["object"] + }, + + // Optional chaining + { + code: "obj?.prop", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[key]", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\nprop", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n?.[key]", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\n[key]", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[\nkey]", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.prop", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[key]", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n?.prop", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n?.[key]", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\n[key]", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[\nkey]", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } } ], invalid: [ @@ -255,6 +317,29 @@ ruleTester.run("dot-location", rule, { output: "(5).\ntoExponential()", options: ["object"], errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 2, column: 1 }] + }, + + // Optional chaining + { + code: "obj\n?.prop", + output: "obj?.\nprop", + options: ["object"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedDotAfterObject" }] + }, + { + code: "10\n?.prop", + output: "10?.\nprop", + options: ["object"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedDotAfterObject" }] + }, + { + code: "obj?.\nprop", + output: "obj\n?.prop", + options: ["property"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedDotBeforeProperty" }] } ] }); diff --git a/tests/lib/rules/dot-notation.js b/tests/lib/rules/dot-notation.js index 5532ea35870..341ef25ec67 100644 --- a/tests/lib/rules/dot-notation.js +++ b/tests/lib/rules/dot-notation.js @@ -218,6 +218,34 @@ ruleTester.run("dot-notation", rule, { output: null, // `let["if"]()` is a syntax error because `let[` indicates a destructuring variable declaration options: [{ allowKeywords: false }], errors: [{ messageId: "useBrackets", data: { key: "if" } }] + }, + + // Optional chaining + { + code: "obj?.['prop']", + output: "obj?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "useDot", data: { key: q("prop") } }] + }, + { + code: "0?.['prop']", + output: "0?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "useDot", data: { key: q("prop") } }] + }, + { + code: "obj?.true", + output: "obj?.[\"true\"]", + options: [{ allowKeywords: false }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "useBrackets", data: { key: "true" } }] + }, + { + code: "let?.true", + output: "let?.[\"true\"]", + options: [{ allowKeywords: false }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "useBrackets", data: { key: "true" } }] } ] }); diff --git a/tests/lib/rules/func-call-spacing.js b/tests/lib/rules/func-call-spacing.js index f0199fd0a4e..3520882b019 100644 --- a/tests/lib/rules/func-call-spacing.js +++ b/tests/lib/rules/func-call-spacing.js @@ -218,6 +218,28 @@ ruleTester.run("func-call-spacing", rule, { code: "import\n(source)", options: ["always", { allowNewlines: true }], parserOptions: { ecmaVersion: 2020 } + }, + + // Optional chaining + { + code: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "func ?.()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "func?. ()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "func ?. ()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 } } ], invalid: [ @@ -560,7 +582,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f\n();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, @@ -572,7 +594,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f\n(a, b);", - output: "f (a, b);", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, @@ -593,7 +615,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f.b\n();", - output: "f.b ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [ { @@ -614,7 +636,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f.b\n().c ();", - output: "f.b ().c ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [ { @@ -635,13 +657,13 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f\n() ()", - output: "f () ()", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, { code: "f\n()()", - output: "f () ()", + output: "f\n() ()", // Don't fix the first error to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [ { messageId: "unexpectedNewline", type: "CallExpression" }, @@ -696,25 +718,25 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f\r();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, { code: "f\u2028();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, { code: "f\u2029();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, { code: "f\r\n();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, @@ -841,7 +863,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "fnn\n (a, b);", - output: "fnn (a, b);", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [ { @@ -853,6 +875,96 @@ ruleTester.run("func-call-spacing", rule, { endColumn: 2 } ] + }, + { + code: "f /*comment*/ ()", + output: null, // Don't remove comments + options: ["never"], + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "f /*\n*/ ()", + output: null, // Don't remove comments + options: ["never"], + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "f/*comment*/()", + output: "f/*comment*/ ()", + options: ["always"], + errors: [{ messageId: "missing" }] + }, + + // Optional chaining + { + code: "func ?.()", + output: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func?. ()", + output: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func ?. ()", + output: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func\n?.()", + output: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func\n//comment\n?.()", + output: null, // Don't remove comments + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func?.()", + output: null, // Not sure inserting a space into either before/after `?.`. + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missing" }] + }, + { + code: "func\n ?.()", + output: "func ?.()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedNewline" }] + }, + { + code: "func?.\n ()", + output: "func?. ()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedNewline" }] + }, + { + code: "func ?.\n ()", + output: "func ?. ()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedNewline" }] + }, + { + code: "func\n /*comment*/ ?.()", + output: null, // Don't remove comments + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedNewline" }] } ] }); diff --git a/tests/lib/rules/func-name-matching.js b/tests/lib/rules/func-name-matching.js index 72ee66b41ed..4add17ea9d0 100644 --- a/tests/lib/rules/func-name-matching.js +++ b/tests/lib/rules/func-name-matching.js @@ -457,6 +457,79 @@ ruleTester.run("func-name-matching", rule, { errors: [ { messageId: "matchProperty", data: { funcName: "bar", name: "value" } } ] + }, + + // Optional chaining + { + code: "(obj?.aaa).foo = function bar() {};", + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "bar", name: "foo" } } + ] + }, + { + code: "Object?.defineProperty(foo, 'bar', { value: function baz() {} })", + options: ["always", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "baz", name: "bar" } } + ] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { value: function baz() {} })", + options: ["always", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "baz", name: "bar" } } + ] + }, + { + code: "Object?.defineProperty(foo, 'bar', { value: function bar() {} })", + options: ["never", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "notMatchProperty", data: { funcName: "bar", name: "bar" } } + ] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { value: function bar() {} })", + options: ["never", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "notMatchProperty", data: { funcName: "bar", name: "bar" } } + ] + }, + { + code: "Object?.defineProperties(foo, { bar: { value: function baz() {} } })", + options: ["always", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "baz", name: "bar" } } + ] + }, + { + code: "(Object?.defineProperties)(foo, { bar: { value: function baz() {} } })", + options: ["always", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "baz", name: "bar" } } + ] + }, + { + code: "Object?.defineProperties(foo, { bar: { value: function bar() {} } })", + options: ["never", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "notMatchProperty", data: { funcName: "bar", name: "bar" } } + ] + }, + { + code: "(Object?.defineProperties)(foo, { bar: { value: function bar() {} } })", + options: ["never", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "notMatchProperty", data: { funcName: "bar", name: "bar" } } + ] } ] }); diff --git a/tests/lib/rules/getter-return.js b/tests/lib/rules/getter-return.js index ea181650dee..7d7cce078ae 100644 --- a/tests/lib/rules/getter-return.js +++ b/tests/lib/rules/getter-return.js @@ -223,6 +223,30 @@ ruleTester.run("getter-return", rule, { { code: "Object.defineProperties(foo, { bar: { get: function () {~function () { return true; }()}} });", options, errors: [{ messageId: "expected" }] }, // option: {allowImplicit: true} - { code: "Object.defineProperty(foo, \"bar\", { get: function (){}});", options, errors: [{ messageId: "expected" }] } + { code: "Object.defineProperty(foo, \"bar\", { get: function (){}});", options, errors: [{ messageId: "expected" }] }, + + // Optional chaining + { + code: "Object?.defineProperty(foo, 'bar', { get: function (){} });", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expected", data: { name: "method 'get'" } }] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { get: function (){} });", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expected", data: { name: "method 'get'" } }] + }, + { + code: "Object?.defineProperty(foo, 'bar', { get: function (){} });", + options, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expected", data: { name: "method 'get'" } }] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { get: function (){} });", + options, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expected", data: { name: "method 'get'" } }] + } ] }); diff --git a/tests/lib/rules/global-require.js b/tests/lib/rules/global-require.js index 3fb6ccca459..4993962bd05 100644 --- a/tests/lib/rules/global-require.js +++ b/tests/lib/rules/global-require.js @@ -30,7 +30,13 @@ const valid = [ { code: "var logger = require(DEBUG ? 'dev-logger' : 'logger');" }, { code: "var logger = DEBUG ? require('dev-logger') : require('logger');" }, { code: "function localScopedRequire(require) { require('y'); }" }, - { code: "var someFunc = require('./someFunc'); someFunc(function(require) { return('bananas'); });" } + { code: "var someFunc = require('./someFunc'); someFunc(function(require) { return('bananas'); });" }, + + // Optional chaining + { + code: "var x = require('y')?.foo;", + parserOptions: { ecmaVersion: 2020 } + } ]; const error = { messageId: "unexpected", type: "CallExpression" }; diff --git a/tests/lib/rules/id-blacklist.js b/tests/lib/rules/id-blacklist.js index 6c3f729e17a..4d13459acf6 100644 --- a/tests/lib/rules/id-blacklist.js +++ b/tests/lib/rules/id-blacklist.js @@ -17,7 +17,7 @@ const rule = require("../../../lib/rules/id-blacklist"), //------------------------------------------------------------------------------ const ruleTester = new RuleTester(); -const error = { messageId: "blacklisted", type: "Identifier" }; +const error = { messageId: "restricted", type: "Identifier" }; ruleTester.run("id-blacklist", rule, { valid: [ @@ -272,7 +272,7 @@ ruleTester.run("id-blacklist", rule, { options: ["bar"], parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 17 @@ -283,7 +283,7 @@ ruleTester.run("id-blacklist", rule, { options: ["foo", "bar"], parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 17 @@ -294,7 +294,7 @@ ruleTester.run("id-blacklist", rule, { options: ["foo"], parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 17 @@ -305,7 +305,7 @@ ruleTester.run("id-blacklist", rule, { options: ["foo"], parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 10 @@ -316,7 +316,7 @@ ruleTester.run("id-blacklist", rule, { options: ["foo"], parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 22 @@ -327,7 +327,7 @@ ruleTester.run("id-blacklist", rule, { options: ["foo"], parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 8 @@ -338,7 +338,7 @@ ruleTester.run("id-blacklist", rule, { options: ["bar"], parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 26 @@ -350,13 +350,13 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 5 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 19 @@ -369,7 +369,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 5 @@ -377,7 +377,7 @@ ruleTester.run("id-blacklist", rule, { // reports each occurrence of local identifier, although it's renamed in this export specifier { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 19 @@ -390,19 +390,19 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 5 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 19 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 26 @@ -415,19 +415,19 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 5 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 19 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 26 @@ -447,7 +447,7 @@ ruleTester.run("id-blacklist", rule, { options: ["bar"], parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 17 @@ -458,7 +458,7 @@ ruleTester.run("id-blacklist", rule, { options: ["foo", "bar"], parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 17 @@ -469,7 +469,7 @@ ruleTester.run("id-blacklist", rule, { options: ["foo"], parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 17 @@ -480,7 +480,7 @@ ruleTester.run("id-blacklist", rule, { options: ["foo"], parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 10 @@ -491,7 +491,7 @@ ruleTester.run("id-blacklist", rule, { options: ["foo"], parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 22 @@ -508,7 +508,7 @@ ruleTester.run("id-blacklist", rule, { code: "foo[bar] = baz;", options: ["bar"], errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier" }] @@ -517,7 +517,7 @@ ruleTester.run("id-blacklist", rule, { code: "baz = foo[bar];", options: ["bar"], errors: [{ - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier" }] @@ -633,7 +633,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 8 @@ -646,7 +646,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 13 @@ -659,13 +659,13 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 9 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 15 @@ -678,7 +678,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "baz" }, type: "Identifier", column: 19 @@ -691,13 +691,13 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 15 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "baz" }, type: "Identifier", column: 21 @@ -710,19 +710,19 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 9 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 17 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "baz" }, type: "Identifier", column: 23 @@ -735,7 +735,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "baz" }, type: "Identifier", column: 21 @@ -748,7 +748,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "qux" }, type: "Identifier", column: 27 @@ -761,7 +761,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 12 @@ -774,7 +774,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "baz" }, type: "Identifier", column: 24 @@ -787,13 +787,13 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 4 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 14 @@ -806,7 +806,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 17 @@ -819,7 +819,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 10 @@ -832,7 +832,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "baz" }, type: "Identifier", column: 18 @@ -845,7 +845,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 10 @@ -858,7 +858,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 11 @@ -871,7 +871,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 17 @@ -884,7 +884,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 19 @@ -897,7 +897,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 9 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 10 @@ -910,7 +910,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 7 @@ -923,7 +923,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "bar" }, type: "Identifier", column: 8 @@ -937,7 +937,7 @@ ruleTester.run("id-blacklist", rule, { options: ["undefined"], errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier" } @@ -948,7 +948,7 @@ ruleTester.run("id-blacklist", rule, { options: ["undefined"], errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier" } @@ -959,7 +959,7 @@ ruleTester.run("id-blacklist", rule, { options: ["undefined"], errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier", column: 13 @@ -972,7 +972,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Number" }, type: "Identifier" } @@ -984,7 +984,7 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Number" }, type: "Identifier" } @@ -996,13 +996,13 @@ ruleTester.run("id-blacklist", rule, { globals: { myGlobal: "readonly" }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "myGlobal" }, type: "Identifier", column: 1 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "myGlobal" }, type: "Identifier", column: 30 @@ -1017,13 +1017,13 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 7 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 22 @@ -1036,13 +1036,13 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 5 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 10 @@ -1054,13 +1054,13 @@ ruleTester.run("id-blacklist", rule, { options: ["foo"], errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 7 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 16 @@ -1072,13 +1072,13 @@ ruleTester.run("id-blacklist", rule, { options: ["foo"], errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 10 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "foo" }, type: "Identifier", column: 29 @@ -1091,13 +1091,13 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Foo" }, type: "Identifier", column: 7 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Foo" }, type: "Identifier", column: 24 @@ -1112,13 +1112,13 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier", column: 5 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier", column: 16 @@ -1130,13 +1130,13 @@ ruleTester.run("id-blacklist", rule, { options: ["undefined"], errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier", column: 7 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier", column: 22 @@ -1148,13 +1148,13 @@ ruleTester.run("id-blacklist", rule, { options: ["undefined"], errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier", column: 10 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier", column: 28 @@ -1167,13 +1167,13 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Number" }, type: "Identifier", column: 7 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Number" }, type: "Identifier", column: 21 @@ -1182,8 +1182,8 @@ ruleTester.run("id-blacklist", rule, { }, /* - * Assignment to a property with a blacklisted name isn't allowed, in general. - * In this case, that restriction prevents creating a global variable with a blacklisted name. + * Assignment to a property with a restricted name isn't allowed, in general. + * In this case, that restriction prevents creating a global variable with a restricted name. */ { code: "/* globals myGlobal */ window.myGlobal = 5; foo = myGlobal;", @@ -1191,7 +1191,7 @@ ruleTester.run("id-blacklist", rule, { env: { browser: true }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "myGlobal" }, type: "Identifier", column: 31 @@ -1206,7 +1206,7 @@ ruleTester.run("id-blacklist", rule, { globals: { undefined: "off" }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier" } @@ -1217,7 +1217,7 @@ ruleTester.run("id-blacklist", rule, { options: ["Number"], errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Number" }, type: "Identifier" } @@ -1228,7 +1228,7 @@ ruleTester.run("id-blacklist", rule, { options: ["Map"], errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Map" }, type: "Identifier" } @@ -1242,13 +1242,13 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier", column: 16 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier", column: 33 @@ -1260,13 +1260,13 @@ ruleTester.run("id-blacklist", rule, { options: ["Number"], errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Number" }, type: "Identifier", column: 14 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Number" }, type: "Identifier", column: 32 @@ -1279,13 +1279,13 @@ ruleTester.run("id-blacklist", rule, { globals: { myGlobal: "readonly" }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "myGlobal" }, type: "Identifier", column: 22 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "myGlobal" }, type: "Identifier", column: 36 @@ -1298,13 +1298,13 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Number" }, type: "Identifier", column: 28 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Number" }, type: "Identifier", column: 58 @@ -1317,13 +1317,13 @@ ruleTester.run("id-blacklist", rule, { parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Number" }, type: "Identifier", column: 8 }, { - messageId: "blacklisted", + messageId: "restricted", data: { name: "Number" }, type: "Identifier", column: 44 @@ -1335,21 +1335,21 @@ ruleTester.run("id-blacklist", rule, { options: ["undefined"], errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier" } ] }, - // this is a reference to a global variable, but at the same time creates a property with a blacklisted name + // this is a reference to a global variable, but at the same time creates a property with a restricted name { code: "var foo = { undefined }", options: ["undefined"], parserOptions: { ecmaVersion: 6 }, errors: [ { - messageId: "blacklisted", + messageId: "restricted", data: { name: "undefined" }, type: "Identifier" } diff --git a/tests/lib/rules/id-denylist.js b/tests/lib/rules/id-denylist.js new file mode 100644 index 00000000000..6da179d99ff --- /dev/null +++ b/tests/lib/rules/id-denylist.js @@ -0,0 +1,1359 @@ +/** + * @fileoverview Tests for id-denylist rule. + * @author Keith Cirkel + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/id-denylist"), + { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); +const error = { messageId: "restricted", type: "Identifier" }; + +ruleTester.run("id-denylist", rule, { + valid: [ + { + code: "foo = \"bar\"", + options: ["bar"] + }, + { + code: "bar = \"bar\"", + options: ["foo"] + }, + { + code: "foo = \"bar\"", + options: ["f", "fo", "fooo", "bar"] + }, + { + code: "function foo(){}", + options: ["bar"] + }, + { + code: "foo()", + options: ["f", "fo", "fooo", "bar"] + }, + { + code: "import { foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: "export { foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: "foo.bar()", + options: ["f", "fo", "fooo", "b", "ba", "baz"] + }, + { + code: "var foo = bar.baz;", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz"] + }, + { + code: "var foo = bar.baz.bing;", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "foo.bar.baz = bing.bong.bash;", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "if (foo.bar) {}", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "var obj = { key: foo.bar };", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "const {foo: bar} = baz", + options: ["foo"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "const {foo: {bar: baz}} = qux", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({ bar: baz }) {}", + options: ["bar"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({ bar: {baz: qux} }) {}", + options: ["bar", "baz"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({baz} = obj.qux) {}", + options: ["qux"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({ foo: {baz} = obj.qux }) {}", + options: ["qux"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "({a: bar = obj.baz});", + options: ["baz"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "({foo: {a: bar = obj.baz}} = qux);", + options: ["baz"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "var arr = [foo.bar];", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "[foo.bar]", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "[foo.bar.nesting]", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "if (foo.bar === bar.baz) { [foo.bar] }", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "var myArray = new Array(); var myDate = new Date();", + options: ["array", "date", "mydate", "myarray", "new", "var"] + }, + { + code: "foo()", + options: ["foo"] + }, + { + code: "foo.bar()", + options: ["bar"] + }, + { + code: "foo.bar", + options: ["bar"] + }, + { + code: "({foo: obj.bar.bar.bar.baz} = {});", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "({[obj.bar]: a = baz} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 } + }, + + // references to global variables + { + code: "Number.parseInt()", + options: ["Number"] + }, + { + code: "x = Number.NaN;", + options: ["Number"] + }, + { + code: "var foo = undefined;", + options: ["undefined"] + }, + { + code: "if (foo === undefined);", + options: ["undefined"] + }, + { + code: "obj[undefined] = 5;", // creates obj["undefined"]. It should be disallowed, but the rule doesn't know values of globals and can't control computed access. + options: ["undefined"] + }, + { + code: "foo = { [myGlobal]: 1 };", + options: ["myGlobal"], + parserOptions: { ecmaVersion: 6 }, + globals: { myGlobal: "readonly" } + }, + { + code: "({ myGlobal } = foo);", // writability doesn't affect the logic, it's always assumed that user doesn't have control over the names of globals. + options: ["myGlobal"], + parserOptions: { ecmaVersion: 6 }, + globals: { myGlobal: "writable" } + }, + { + code: "/* global myGlobal: readonly */ myGlobal = 5;", + options: ["myGlobal"] + }, + { + code: "var foo = [Map];", + options: ["Map"], + env: { es6: true } + }, + { + code: "var foo = { bar: window.baz };", + options: ["window"], + env: { browser: true } + } + ], + invalid: [ + { + code: "foo = \"bar\"", + options: ["foo"], + errors: [ + error + ] + }, + { + code: "bar = \"bar\"", + options: ["bar"], + errors: [ + error + ] + }, + { + code: "foo = \"bar\"", + options: ["f", "fo", "foo", "bar"], + errors: [ + error + ] + }, + { + code: "function foo(){}", + options: ["f", "fo", "foo", "bar"], + errors: [ + error + ] + }, + { + code: "import foo from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "import * as foo from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "export * as foo from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "import { foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "import { foo as bar } from 'mod'", + options: ["bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "import { foo as bar } from 'mod'", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "import { foo as foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "import { foo, foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + }] + }, + { + code: "import { foo as bar, foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 22 + }] + }, + { + code: "import foo, { foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 8 + }] + }, + { + code: "var foo; export { foo as bar };", + options: ["bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 26 + }] + }, + { + code: "var foo; export { foo };", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "var foo; export { foo as bar };", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + + // reports each occurrence of local identifier, although it's renamed in this export specifier + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "var foo; export { foo as foo };", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 26 + } + ] + }, + { + code: "var foo; export { foo as bar };", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 26 + } + ] + }, + { + code: "export { foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "export { foo as bar } from 'mod'", + options: ["bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "export { foo as bar } from 'mod'", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "export { foo as foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "export { foo, foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + }] + }, + { + code: "export { foo as bar, foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 22 + }] + }, + { + code: "foo.bar()", + options: ["f", "fo", "foo", "b", "ba", "baz"], + errors: [ + error + ] + }, + { + code: "foo[bar] = baz;", + options: ["bar"], + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier" + }] + }, + { + code: "baz = foo[bar];", + options: ["bar"], + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier" + }] + }, + { + code: "var foo = bar.baz;", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz"], + errors: [ + error + ] + }, + { + code: "var foo = bar.baz;", + options: ["f", "fo", "fooo", "b", "ba", "bar", "bazz"], + errors: [ + error + ] + }, + { + code: "if (foo.bar) {}", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "var obj = { key: foo.bar };", + options: ["obj"], + errors: [ + error + ] + }, + { + code: "var obj = { key: foo.bar };", + options: ["key"], + errors: [ + error + ] + }, + { + code: "var obj = { key: foo.bar };", + options: ["foo"], + errors: [ + error + ] + }, + { + code: "var arr = [foo.bar];", + options: ["arr"], + errors: [ + error + ] + }, + { + code: "var arr = [foo.bar];", + options: ["foo"], + errors: [ + error + ] + }, + { + code: "[foo.bar]", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "if (foo.bar === bar.baz) { [bing.baz] }", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "if (foo.bar === bar.baz) { [foo.bar] }", + options: ["f", "fo", "fooo", "b", "ba", "bar", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "var myArray = new Array(); var myDate = new Date();", + options: ["array", "date", "myDate", "myarray", "new", "var"], + errors: [ + error + ] + }, + { + code: "var myArray = new Array(); var myDate = new Date();", + options: ["array", "date", "mydate", "myArray", "new", "var"], + errors: [ + error + ] + }, + { + code: "foo.bar = 1", + options: ["bar"], + errors: [ + error + ] + }, + { + code: "foo.bar.baz = 1", + options: ["bar", "baz"], + errors: [ + error + ] + }, + { + code: "const {foo} = baz", + options: ["foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 8 + } + ] + }, + { + code: "const {foo: bar} = baz", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 13 + } + ] + }, + { + code: "const {[foo]: bar} = baz", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 9 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 15 + } + ] + }, + { + code: "const {foo: {bar: baz}} = qux", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "const {foo: {[bar]: baz}} = qux", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 15 + }, + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 21 + } + ] + }, + { + code: "const {[foo]: {[bar]: baz}} = qux", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 9 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }, + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 23 + } + ] + }, + { + code: "function foo({ bar: baz }) {}", + options: ["bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 21 + } + ] + }, + { + code: "function foo({ bar: {baz: qux} }) {}", + options: ["bar", "baz", "qux"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "qux" }, + type: "Identifier", + column: 27 + } + ] + }, + { + code: "({foo: obj.bar} = baz);", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 12 + } + ] + }, + { + code: "({foo: obj.bar.bar.bar.baz} = {});", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 24 + } + ] + }, + { + code: "({[foo]: obj.bar} = baz);", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 4 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 14 + } + ] + }, + { + code: "({foo: { a: obj.bar }} = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + } + ] + }, + { + code: "({a: obj.bar = baz} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "({a: obj.bar.bar.baz = obj.qux} = obj.qux);", + options: ["a", "bar", "baz", "qux"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 18 + } + ] + }, + { + code: "({a: obj[bar] = obj.qux} = obj.qux);", + options: ["a", "bar", "baz", "qux"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "({a: [obj.bar] = baz} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 11 + } + ] + }, + { + code: "({foo: { a: obj.bar = baz}} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + } + ] + }, + { + code: "({foo: { [a]: obj.bar }} = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "({...obj.bar} = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 9 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "([obj.bar] = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 7 + } + ] + }, + { + code: "const [bar] = baz;", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 8 + } + ] + }, + + // not a reference to a global variable, because it isn't a reference to a variable + { + code: "foo.undefined = 1;", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { undefined: 1 };", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { undefined: undefined };", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 13 + } + ] + }, + { + code: "var foo = { Number() {} };", + options: ["Number"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier" + } + ] + }, + { + code: "class Foo { Number() {} }", + options: ["Number"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier" + } + ] + }, + { + code: "myGlobal: while(foo) { break myGlobal; } ", + options: ["myGlobal"], + globals: { myGlobal: "readonly" }, + errors: [ + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 1 + }, + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 30 + } + ] + }, + + // globals declared in the given source code are not excluded from consideration + { + code: "const foo = 1; bar = foo;", + options: ["foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 22 + } + ] + }, + { + code: "let foo; foo = bar;", + options: ["foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "bar = foo; var foo;", + options: ["foo"], + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 16 + } + ] + }, + { + code: "function foo() {} var bar = foo;", + options: ["foo"], + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 29 + } + ] + }, + { + code: "class Foo {} var bar = Foo;", + options: ["Foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Foo" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "Foo" }, + type: "Identifier", + column: 24 + } + ] + }, + + // redeclared globals are not excluded from consideration + { + code: "let undefined; undefined = 1;", + options: ["undefined"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 16 + } + ] + }, + { + code: "foo = undefined; var undefined;", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 22 + } + ] + }, + { + code: "function undefined(){} x = undefined;", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 10 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 28 + } + ] + }, + { + code: "class Number {} x = Number.NaN;", + options: ["Number"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 21 + } + ] + }, + + /* + * Assignment to a property with a restricted name isn't allowed, in general. + * In this case, that restriction prevents creating a global variable with a restricted name. + */ + { + code: "/* globals myGlobal */ window.myGlobal = 5; foo = myGlobal;", + options: ["myGlobal"], + env: { browser: true }, + errors: [ + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 31 + } + ] + }, + + // disabled global variables + { + code: "var foo = undefined;", + options: ["undefined"], + globals: { undefined: "off" }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + { + code: "/* globals Number: off */ Number.parseInt()", + options: ["Number"], + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = [Map];", // this actually isn't a disabled global: it was never enabled because es6 environment isn't enabled + options: ["Map"], + errors: [ + { + messageId: "restricted", + data: { name: "Map" }, + type: "Identifier" + } + ] + }, + + // shadowed global variables + { + code: "if (foo) { let undefined; bar = undefined; }", + options: ["undefined"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 16 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 33 + } + ] + }, + { + code: "function foo(Number) { var x = Number.NaN; }", + options: ["Number"], + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 14 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 32 + } + ] + }, + { + code: "function foo() { var myGlobal; x = myGlobal; }", + options: ["myGlobal"], + globals: { myGlobal: "readonly" }, + errors: [ + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 22 + }, + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 36 + } + ] + }, + { + code: "function foo(bar) { return Number.parseInt(bar); } const Number = 1;", + options: ["Number"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 28 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 58 + } + ] + }, + { + code: "import Number from 'myNumber'; const foo = Number.parseInt(bar);", + options: ["Number"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 8 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 44 + } + ] + }, + { + code: "var foo = function undefined() {};", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + + // this is a reference to a global variable, but at the same time creates a property with a restricted name + { + code: "var foo = { undefined }", + options: ["undefined"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + } + ] +}); diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js index 56589be5169..b253be22067 100644 --- a/tests/lib/rules/indent.js +++ b/tests/lib/rules/indent.js @@ -11374,6 +11374,105 @@ ruleTester.run("indent", rule, { [5, 4, 0, "Identifier"], [6, 0, 4, "Punctuator"] ]) + }, + + // Optional chaining + { + code: unIndent` + obj + ?.prop + ?.[key] + ?. + [key] + `, + output: unIndent` + obj + ?.prop + ?.[key] + ?. + [key] + `, + options: [4], + parserOptions: { ecmaVersion: 2020 }, + errors: expectedErrors([ + [2, 4, 0, "Punctuator"], + [3, 4, 0, "Punctuator"], + [4, 4, 0, "Punctuator"], + [5, 8, 0, "Punctuator"] + ]) + }, + { + code: unIndent` + ( + longSomething + ?.prop + ?.[key] + ) + ?.prop + ?.[key] + `, + output: unIndent` + ( + longSomething + ?.prop + ?.[key] + ) + ?.prop + ?.[key] + `, + options: [4], + parserOptions: { ecmaVersion: 2020 }, + errors: expectedErrors([ + [6, 4, 0, "Punctuator"], + [7, 4, 0, "Punctuator"] + ]) + }, + { + code: unIndent` + obj + ?.(arg) + ?. + (arg) + `, + output: unIndent` + obj + ?.(arg) + ?. + (arg) + `, + options: [4], + parserOptions: { ecmaVersion: 2020 }, + errors: expectedErrors([ + [2, 4, 0, "Punctuator"], + [3, 4, 0, "Punctuator"], + [4, 4, 0, "Punctuator"] + ]) + }, + { + code: unIndent` + ( + longSomething + ?.(arg) + ?.(arg) + ) + ?.(arg) + ?.(arg) + `, + output: unIndent` + ( + longSomething + ?.(arg) + ?.(arg) + ) + ?.(arg) + ?.(arg) + `, + options: [4], + parserOptions: { ecmaVersion: 2020 }, + errors: expectedErrors([ + [6, 4, 0, "Punctuator"], + [7, 4, 0, "Punctuator"] + ]) } ] }); diff --git a/tests/lib/rules/keyword-spacing.js b/tests/lib/rules/keyword-spacing.js index b67ee52384b..64ee5a43474 100644 --- a/tests/lib/rules/keyword-spacing.js +++ b/tests/lib/rules/keyword-spacing.js @@ -1364,14 +1364,14 @@ ruleTester.run("keyword-spacing", rule, { code: "import *as a from \"foo\"", output: "import * as a from \"foo\"", parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: expectedBefore("as") - }, - { - code: "import* as a from\"foo\"", - output: "import*as a from\"foo\"", - options: [NEITHER], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: unexpectedBefore("as") + errors: [{ + messageId: "expectedBefore", + data: { value: "as" }, + line: 1, + column: 9, + endLine: 1, + endColumn: 11 + }] }, { code: "import* as a from\"foo\"", @@ -1380,27 +1380,26 @@ ruleTester.run("keyword-spacing", rule, { parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ messageId: "unexpectedBefore", + data: { value: "as" }, line: 1, column: 8, endLine: 1, endColumn: 9 - } - ] + }] }, { - code: "import *as a from\"foo\"", + code: "import* as a from\"foo\"", output: "import*as a from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [ - { - messageId: "unexpectedAfter", - line: 1, - column: 7, - endLine: 1, - endColumn: 8 - } - ] + errors: [{ + messageId: "unexpectedBefore", + data: { value: "as" }, + line: 1, + column: 8, + endLine: 1, + endColumn: 11 + }] }, { code: "import*as a from\"foo\"", @@ -2510,6 +2509,47 @@ ruleTester.run("keyword-spacing", rule, { // import //---------------------------------------------------------------------- + { + code: "import* as a from \"foo\"", + output: "import * as a from \"foo\"", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "expectedAfter", + data: { value: "import" }, + line: 1, + column: 1, + endLine: 1, + endColumn: 7 + }] + }, + { + code: "import *as a from\"foo\"", + output: "import*as a from\"foo\"", + options: [NEITHER], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "unexpectedAfter", + data: { value: "import" }, + line: 1, + column: 7, + endLine: 1, + endColumn: 8 + }] + }, + { + code: "import *as a from\"foo\"", + output: "import*as a from\"foo\"", + options: [NEITHER], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "unexpectedAfter", + data: { value: "import" }, + line: 1, + column: 7, + endLine: 1, + endColumn: 10 + }] + }, { code: "{}import{a} from \"foo\"", output: "{} import {a} from \"foo\"", diff --git a/tests/lib/rules/max-len.js b/tests/lib/rules/max-len.js index f6296b636b9..557b5506f89 100644 --- a/tests/lib/rules/max-len.js +++ b/tests/lib/rules/max-len.js @@ -341,7 +341,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 86, maxLength: 80 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 30 } ] }, @@ -354,7 +356,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 24, maxLength: 10 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 25 } ] }, @@ -367,7 +371,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 22, maxLength: 15 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 14 } ] }, @@ -380,14 +386,18 @@ ruleTester.run("max-len", rule, { data: { lineLength: 22, maxLength: 15 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 14 }, { messageId: "max", data: { lineLength: 22, maxLength: 15 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 14 } ] }, @@ -400,7 +410,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 56, maxLength: 20 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 57 } ] }, @@ -415,7 +427,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 54, maxLength: 20 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 55 } ] }, @@ -428,7 +442,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 30, maxLength: 10 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 31 } ] }, @@ -441,7 +457,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 62, maxLength: 40 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 63 } ] }, @@ -454,7 +472,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 57, maxLength: 40 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 58 } ] }, { @@ -466,7 +486,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 53, maxLength: 40 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 54 } ] }, { @@ -478,7 +500,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 49, maxCommentLength: 20 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 50 } ] }, { @@ -490,7 +514,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 119, maxCommentLength: 80 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 120 } ] }, { @@ -502,7 +528,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 49, maxLength: 20 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 50 } ] }, { @@ -514,7 +542,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 73, maxLength: 40 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 74 } ] }, @@ -531,7 +561,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 29, maxCommentLength: 28 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 30 } ] }, { @@ -545,7 +577,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 33, maxCommentLength: 32 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 34 } ] }, { @@ -560,14 +594,18 @@ ruleTester.run("max-len", rule, { data: { lineLength: 29, maxCommentLength: 28 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 30 }, { messageId: "maxComment", data: { lineLength: 32, maxCommentLength: 28 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 33 } ] }, { @@ -582,14 +620,18 @@ ruleTester.run("max-len", rule, { data: { lineLength: 33, maxCommentLength: 32 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 34 }, { messageId: "maxComment", data: { lineLength: 36, maxCommentLength: 32 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 37 } ] }, { @@ -604,14 +646,18 @@ ruleTester.run("max-len", rule, { data: { lineLength: 40, maxLength: 39 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 41 }, { messageId: "maxComment", data: { lineLength: 36, maxCommentLength: 35 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 37 } ] }, { @@ -626,14 +672,18 @@ ruleTester.run("max-len", rule, { data: { lineLength: 33, maxCommentLength: 32 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 34 }, { messageId: "max", data: { lineLength: 43, maxLength: 42 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 44 } ] }, @@ -649,7 +699,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 51, maxLength: 20 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 52 } ] }, @@ -664,7 +716,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 39, maxLength: 29 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 40 } ] }, @@ -677,7 +731,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 45, maxLength: 29 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 46 } ] }, @@ -690,7 +746,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 57, maxLength: 29 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 58 } ] }, @@ -703,7 +761,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 39, maxLength: 29 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 40 } ] }, @@ -717,7 +777,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 39, maxLength: 29 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 40 } ] }, @@ -731,14 +793,18 @@ ruleTester.run("max-len", rule, { data: { lineLength: 37, maxLength: 29 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 38 }, { messageId: "max", data: { lineLength: 44, maxLength: 29 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 45 } ] }, @@ -752,7 +818,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 58, maxLength: 29 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 59 } ] }, @@ -767,7 +835,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 12, maxLength: 10 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 21 } ] }, @@ -781,7 +851,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 1, maxLength: 0 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 2 } ] }, @@ -799,7 +871,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 38, maxCommentLength: 37 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 39 } ] }, @@ -815,7 +889,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 44, maxCommentLength: 40 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 39 } ] }, @@ -831,7 +907,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 38, maxLength: 15 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 39 } ] }, @@ -847,7 +925,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 38, maxLength: 37 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 39 } ] }, @@ -863,7 +943,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 38, maxLength: 37 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 39 } ] }, @@ -879,7 +961,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 50, maxLength: 49 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 51 } ] }, @@ -896,7 +980,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 44, maxLength: 37 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 45 } ] }, @@ -912,7 +998,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 57, maxLength: 56 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 58 } ] }, @@ -928,7 +1016,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 57, maxLength: 56 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 58 } ] }, @@ -944,7 +1034,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 56, maxLength: 55 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 57 } ] }, @@ -961,7 +1053,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 51, maxLength: 30 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 52 } ] }, @@ -978,7 +1072,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 80, maxLength: 79 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 81 } ] }, @@ -994,7 +1090,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 87, maxLength: 85 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 88 } ] }, @@ -1011,7 +1109,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 87, maxLength: 37 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 88 } ] }, @@ -1028,7 +1128,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 119, maxLength: 37 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 120 } ] }, @@ -1045,7 +1147,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 55, maxLength: 37 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 56 } ] }, @@ -1062,7 +1166,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 55, maxLength: 37 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 56 } ] }, @@ -1079,7 +1185,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 15, maxLength: 14 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 16 } ] }, @@ -1096,7 +1204,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 31, maxLength: 30 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 32 } ] } diff --git a/tests/lib/rules/new-cap.js b/tests/lib/rules/new-cap.js index ec85c4bf6fd..5953c87c24a 100644 --- a/tests/lib/rules/new-cap.js +++ b/tests/lib/rules/new-cap.js @@ -71,7 +71,39 @@ ruleTester.run("new-cap", rule, { { code: "var x = new foo.bar(42);", options: [{ newIsCapExceptionPattern: "^foo\\.." }] }, { code: "var x = new foo.bar(42);", options: [{ properties: false }] }, { code: "var x = Foo.bar(42);", options: [{ properties: false }] }, - { code: "var x = foo.Bar(42);", options: [{ capIsNew: false, properties: false }] } + { code: "var x = foo.Bar(42);", options: [{ capIsNew: false, properties: false }] }, + + // Optional chaining + { + code: "foo?.bar();", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "(foo?.bar)();", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "new (foo?.Bar)();", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "(foo?.Bar)();", + options: [{ properties: false }], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "new (foo?.bar)();", + options: [{ properties: false }], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "Date?.UTC();", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "(Date?.UTC)();", + parserOptions: { ecmaVersion: 2020 } + } ], invalid: [ { @@ -302,6 +334,23 @@ ruleTester.run("new-cap", rule, { options: [{ newIsCapExceptionPattern: "^foo\\.." }], errors: [{ type: "NewExpression", messageId: "lower" }] + }, + + // Optional chaining + { + code: "new (foo?.bar)();", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "lower", column: 11, endColumn: 14 }] + }, + { + code: "foo?.Bar();", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "upper", column: 6, endColumn: 9 }] + }, + { + code: "(foo?.Bar)();", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "upper", column: 7, endColumn: 10 }] } ] }); diff --git a/tests/lib/rules/newline-per-chained-call.js b/tests/lib/rules/newline-per-chained-call.js index 28d45c69b82..2a26937ae62 100644 --- a/tests/lib/rules/newline-per-chained-call.js +++ b/tests/lib/rules/newline-per-chained-call.js @@ -340,5 +340,69 @@ ruleTester.run("newline-per-chained-call", rule, { endLine: 1, endColumn: 35 }] - }] + }, + + // Optional chaining + { + code: "obj?.foo1()?.foo2()?.foo3()", + output: "obj?.foo1()\n?.foo2()\n?.foo3()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.foo2" } }, + { messageId: "expected", data: { callee: "?.foo3" } } + ] + }, + { + code: "(obj?.foo1()?.foo2)()?.foo3()", + output: "(obj?.foo1()\n?.foo2)()\n?.foo3()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.foo2" } }, + { messageId: "expected", data: { callee: "?.foo3" } } + ] + }, + { + code: "(obj?.foo1())?.foo2()?.foo3()", + output: "(obj?.foo1())\n?.foo2()\n?.foo3()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.foo2" } }, + { messageId: "expected", data: { callee: "?.foo3" } } + ] + }, + { + code: "obj?.[foo1]()?.[foo2]()?.[foo3]()", + output: "obj?.[foo1]()\n?.[foo2]()\n?.[foo3]()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.[foo2]" } }, + { messageId: "expected", data: { callee: "?.[foo3]" } } + ] + }, + { + code: "(obj?.[foo1]()?.[foo2])()?.[foo3]()", + output: "(obj?.[foo1]()\n?.[foo2])()\n?.[foo3]()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.[foo2]" } }, + { messageId: "expected", data: { callee: "?.[foo3]" } } + ] + }, + { + code: "(obj?.[foo1]())?.[foo2]()?.[foo3]()", + output: "(obj?.[foo1]())\n?.[foo2]()\n?.[foo3]()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.[foo2]" } }, + { messageId: "expected", data: { callee: "?.[foo3]" } } + ] + } + + ] }); diff --git a/tests/lib/rules/no-alert.js b/tests/lib/rules/no-alert.js index 4851d5b0a7a..03fcddb6994 100644 --- a/tests/lib/rules/no-alert.js +++ b/tests/lib/rules/no-alert.js @@ -124,6 +124,18 @@ ruleTester.run("no-alert", rule, { code: "function foo() { var globalThis = bar; globalThis.alert(); }\nglobalThis.alert();", env: { es2020: true }, errors: [{ messageId: "unexpected", data: { name: "alert" }, type: "CallExpression", line: 2, column: 1 }] + }, + + // Optional chaining + { + code: "window?.alert(foo)", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { name: "alert" } }] + }, + { + code: "(window?.alert)(foo)", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { name: "alert" } }] } ] }); diff --git a/tests/lib/rules/no-duplicate-case.js b/tests/lib/rules/no-duplicate-case.js index a6e8e0ff34c..42dbda3f18d 100644 --- a/tests/lib/rules/no-duplicate-case.js +++ b/tests/lib/rules/no-duplicate-case.js @@ -128,6 +128,66 @@ ruleTester.run("no-duplicate-case", rule, { column: 74 } ] + }, + { + code: "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p. p // comment\n .p1: break; default: break;}", + errors: [{ + messageId: "unexpected", + type: "SwitchCase", + column: 69 + }] + }, + { + code: "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p .p\n/* comment */\n.p1: break; case p.p.p1: break; default: break;}", + errors: [{ + messageId: "unexpected", + type: "SwitchCase", + line: 3, + column: 13 + }] + }, + { + code: "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p .p\n/* comment */\n.p1: break; case p. p // comment\n .p1: break; default: break;}", + errors: [{ + messageId: "unexpected", + type: "SwitchCase", + line: 3, + column: 13 + }] + }, + { + code: "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p. p // comment\n .p1: break; case p .p\n/* comment */\n.p1: break; default: break;}", + errors: [ + { + messageId: "unexpected", + type: "SwitchCase", + line: 1, + column: 69 + }, + { + messageId: "unexpected", + type: "SwitchCase", + line: 2, + column: 14 + } + ] + }, + { + code: "var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a + 1).p1: break; case f(a+1).p1: break; default: break;}", + errors: [{ + messageId: "unexpected", + type: "SwitchCase", + column: 87 + }] + }, + { + code: "var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(\na + 1 // comment\n).p1: break; case f(a+1)\n.p1: break; default: break;}", + errors: [{ + messageId: "unexpected", + type: "SwitchCase", + line: 3, + column: 14 + }] } ] }); diff --git a/tests/lib/rules/no-eval.js b/tests/lib/rules/no-eval.js index ad184bd9370..bc8d38471bb 100644 --- a/tests/lib/rules/no-eval.js +++ b/tests/lib/rules/no-eval.js @@ -67,7 +67,10 @@ ruleTester.run("no-eval", rule, { { code: "(0, globalThis['eval'])('foo')", options: [{ allowIndirect: true }], env: { es2020: true } }, { code: "var EVAL = globalThis.eval; EVAL('foo')", options: [{ allowIndirect: true }] }, { code: "function foo() { globalThis.eval('foo') }", options: [{ allowIndirect: true }], env: { es2020: true } }, - { code: "globalThis.globalThis.eval('foo');", options: [{ allowIndirect: true }], env: { es2020: true } } + { code: "globalThis.globalThis.eval('foo');", options: [{ allowIndirect: true }], env: { es2020: true } }, + { code: "eval?.('foo')", options: [{ allowIndirect: true }], parserOptions: { ecmaVersion: 2020 } }, + { code: "window?.eval('foo')", options: [{ allowIndirect: true }], parserOptions: { ecmaVersion: 2020 }, env: { browser: true } }, + { code: "(window?.eval)('foo')", options: [{ allowIndirect: true }], parserOptions: { ecmaVersion: 2020 }, env: { browser: true } } ], invalid: [ @@ -100,6 +103,26 @@ ruleTester.run("no-eval", rule, { { code: "globalThis.globalThis.eval('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "CallExpression", column: 23, endColumn: 27 }] }, { code: "globalThis.globalThis['eval']('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "CallExpression", column: 23, endColumn: 29 }] }, { code: "(0, globalThis.eval)('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "MemberExpression", column: 16, endColumn: 20 }] }, - { code: "(0, globalThis['eval'])('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "MemberExpression", column: 16, endColumn: 22 }] } + { code: "(0, globalThis['eval'])('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "MemberExpression", column: 16, endColumn: 22 }] }, + + // Optional chaining + { + code: "window?.eval('foo')", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "(window?.eval)('foo')", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "(window?.window).eval('foo')", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "unexpected" }] + } ] }); diff --git a/tests/lib/rules/no-extend-native.js b/tests/lib/rules/no-extend-native.js index a38177431d2..db07c123314 100644 --- a/tests/lib/rules/no-extend-native.js +++ b/tests/lib/rules/no-extend-native.js @@ -37,6 +37,7 @@ ruleTester.run("no-extend-native", rule, { code: "Object.prototype.g = 0", options: [{ exceptions: ["Object"] }] }, + "obj[Object.prototype] = 0", // https://github.com/eslint/eslint/issues/4438 "Object.defineProperty()", @@ -137,5 +138,29 @@ ruleTester.run("no-extend-native", rule, { data: { builtin: "Object" }, type: "AssignmentExpression" }] - }] + }, + + // Optional chaining + { + code: "(Object?.prototype).p = 0", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + }, + { + code: "Object.defineProperty(Object?.prototype, 'p', { value: 0 })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + }, + { + code: "Object?.defineProperty(Object.prototype, 'p', { value: 0 })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + }, + { + code: "(Object?.defineProperty)(Object.prototype, 'p', { value: 0 })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + } + + ] }); diff --git a/tests/lib/rules/no-extra-bind.js b/tests/lib/rules/no-extra-bind.js index 12197967176..8422f3de771 100644 --- a/tests/lib/rules/no-extra-bind.js +++ b/tests/lib/rules/no-extra-bind.js @@ -102,6 +102,16 @@ ruleTester.run("no-extra-bind", rule, { output: "var a = function() { (function(){ (function(){ this.d }.bind(c)) }) }", errors: [{ messageId: "unexpected", type: "CallExpression", column: 71 }] }, + { + code: "var a = (function() { return 1; }).bind(this)", + output: "var a = (function() { return 1; })", + errors + }, + { + code: "var a = (function() { return 1; }.bind)(this)", + output: "var a = (function() { return 1; })", + errors + }, // Should not autofix if bind expression args have side effects { @@ -180,6 +190,44 @@ ruleTester.run("no-extra-bind", rule, { code: "var a = function() {}.bind(b)/**/", output: "var a = function() {}/**/", errors + }, + + // Optional chaining + { + code: "var a = function() { return 1; }.bind?.(b)", + output: "var a = function() { return 1; }", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = function() { return 1; }?.bind(b)", + output: "var a = function() { return 1; }", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = (function() { return 1; }?.bind)(b)", + output: "var a = (function() { return 1; })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = function() { return 1; }['bind']?.(b)", + output: "var a = function() { return 1; }", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = function() { return 1; }?.['bind'](b)", + output: "var a = function() { return 1; }", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = (function() { return 1; }?.['bind'])(b)", + output: "var a = (function() { return 1; })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] } ] }); diff --git a/tests/lib/rules/no-extra-boolean-cast.js b/tests/lib/rules/no-extra-boolean-cast.js index 70ec8bd4f61..8dda6992d21 100644 --- a/tests/lib/rules/no-extra-boolean-cast.js +++ b/tests/lib/rules/no-extra-boolean-cast.js @@ -2408,6 +2408,21 @@ ruleTester.run("no-extra-boolean-cast", rule, { options: [{ enforceForLogicalOperands: true }], parserOptions: { ecmaVersion: 2020 }, errors: [{ messageId: "unexpectedCall", type: "CallExpression" }] + }, + + // Optional chaining + { + code: "if (Boolean?.(foo)) ;", + output: "if (foo) ;", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedCall" }] + }, + { + code: "if (Boolean?.(a ?? b) || c) {}", + output: "if ((a ?? b) || c) {}", + options: [{ enforceForLogicalOperands: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedCall" }] } ] }); diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js index 91099f8cba0..6d7f8ef7da0 100644 --- a/tests/lib/rules/no-extra-parens.js +++ b/tests/lib/rules/no-extra-parens.js @@ -612,6 +612,14 @@ ruleTester.run("no-extra-parens", rule, { "for (; a; a); a; a;", "for (let a = (b && c) === d; ;);", + "new (a()).b.c;", + "new (a().b).c;", + "new (a().b.c);", + "new (a().b().d);", + "new a().b().d;", + "new (a(b()).c)", + "new (a.b()).c", + // Nullish coalescing { code: "var v = (a ?? b) || c", parserOptions: { ecmaVersion: 2020 } }, { code: "var v = a ?? (b || c)", parserOptions: { ecmaVersion: 2020 } }, @@ -620,7 +628,32 @@ ruleTester.run("no-extra-parens", rule, { { code: "var v = (a || b) ?? c", parserOptions: { ecmaVersion: 2020 } }, { code: "var v = a || (b ?? c)", parserOptions: { ecmaVersion: 2020 } }, { code: "var v = (a && b) ?? c", parserOptions: { ecmaVersion: 2020 } }, - { code: "var v = a && (b ?? c)", parserOptions: { ecmaVersion: 2020 } } + { code: "var v = a && (b ?? c)", parserOptions: { ecmaVersion: 2020 } }, + + // Optional chaining + { code: "var v = (obj?.aaa).bbb", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.aaa)()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = new (obj?.aaa)()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = new (obj?.aaa)", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.aaa)`template`", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.()).bbb", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.())()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = new (obj?.())()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = new (obj?.())", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.())`template`", parserOptions: { ecmaVersion: 2020 } }, + { code: "(obj?.aaa).bbb = 0", parserOptions: { ecmaVersion: 2020 } }, + { code: "var foo = (function(){})?.()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var foo = (function(){}?.())", parserOptions: { ecmaVersion: 2020 } }, + { + code: "var foo = (function(){})?.call()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var foo = (function(){}?.call())", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + parserOptions: { ecmaVersion: 2020 } + } ], invalid: [ @@ -759,6 +792,7 @@ ruleTester.run("no-extra-parens", rule, { invalid("(new foo(bar)).baz", "new foo(bar).baz", "NewExpression"), invalid("(new foo.bar()).baz", "new foo.bar().baz", "NewExpression"), invalid("(new foo.bar()).baz()", "new foo.bar().baz()", "NewExpression"), + invalid("new a[(b()).c]", "new a[b().c]", "CallExpression"), invalid("(a)()", "a()", "Identifier"), invalid("(a.b)()", "a.b()", "MemberExpression"), @@ -772,6 +806,14 @@ ruleTester.run("no-extra-parens", rule, { invalid("((new A))()", "(new A)()", "NewExpression"), invalid("new (foo\n.baz\n.bar\n.foo.baz)", "new foo\n.baz\n.bar\n.foo.baz", "MemberExpression"), invalid("new (foo.baz.bar.baz)", "new foo.baz.bar.baz", "MemberExpression"), + invalid("new ((a.b())).c", "new (a.b()).c", "CallExpression"), + invalid("new ((a().b)).c", "new (a().b).c", "MemberExpression"), + invalid("new ((a().b().d))", "new (a().b().d)", "MemberExpression"), + invalid("new ((a())).b.d", "new (a()).b.d", "CallExpression"), + invalid("new (a.b).d;", "new a.b.d;", "MemberExpression"), + invalid("(a().b).d;", "a().b.d;", "MemberExpression"), + invalid("(a.b()).d;", "a.b().d;", "CallExpression"), + invalid("(a.b).d;", "a.b.d;", "MemberExpression"), invalid("0, (_ => 0)", "0, _ => 0", "ArrowFunctionExpression", 1), invalid("(_ => 0), 0", "_ => 0, 0", "ArrowFunctionExpression", 1), @@ -2698,6 +2740,34 @@ ruleTester.run("no-extra-parens", rule, { output: "var v = a | b ?? c | d", parserOptions: { ecmaVersion: 2020 }, errors: [{ messageId: "unexpected" }] + }, + + // Optional chaining + { + code: "var v = (obj?.aaa)?.aaa", + output: "var v = obj?.aaa?.aaa", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var v = (obj.aaa)?.aaa", + output: "var v = obj.aaa?.aaa", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var foo = (function(){})?.call()", + output: "var foo = function(){}?.call()", + options: ["all", { enforceForFunctionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var foo = (function(){}?.call())", + output: "var foo = function(){}?.call()", + options: ["all", { enforceForFunctionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] } ] }); diff --git a/tests/lib/rules/no-implicit-coercion.js b/tests/lib/rules/no-implicit-coercion.js index 73bfe7df6d8..fa2b68b4975 100644 --- a/tests/lib/rules/no-implicit-coercion.js +++ b/tests/lib/rules/no-implicit-coercion.js @@ -355,6 +355,28 @@ ruleTester.run("no-implicit-coercion", rule, { data: { recommendation: "String(1n)" }, type: "BinaryExpression" }] + }, + + // Optional chaining + { + code: "~foo?.indexOf(1)", + output: null, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "useRecommendation", + data: { recommendation: "foo?.indexOf(1) >= 0" }, + type: "UnaryExpression" + }] + }, + { + code: "~(foo?.indexOf)(1)", + output: null, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "useRecommendation", + data: { recommendation: "(foo?.indexOf)(1) !== -1" }, + type: "UnaryExpression" + }] } ] }); diff --git a/tests/lib/rules/no-implied-eval.js b/tests/lib/rules/no-implied-eval.js index 1c712f450f0..ce06f662043 100644 --- a/tests/lib/rules/no-implied-eval.js +++ b/tests/lib/rules/no-implied-eval.js @@ -235,6 +235,20 @@ ruleTester.run("no-implied-eval", rule, { line: 3 } ] + }, + + // Optional chaining + { + code: "window?.setTimeout('code', 0)", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "impliedEval" }] + }, + { + code: "(window?.setTimeout)('code', 0)", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "impliedEval" }] } ] }); diff --git a/tests/lib/rules/no-import-assign.js b/tests/lib/rules/no-import-assign.js index ab65451e33a..babfdfc3445 100644 --- a/tests/lib/rules/no-import-assign.js +++ b/tests/lib/rules/no-import-assign.js @@ -310,6 +310,23 @@ ruleTester.run("no-import-assign", rule, { { code: "import mod, * as mod_ns from 'mod'; mod.prop = 0; mod_ns.prop = 0", errors: [{ messageId: "readonlyMember", data: { name: "mod_ns" }, column: 51 }] + }, + + // Optional chaining + { + code: "import * as mod from 'mod'; Object?.defineProperty(mod, key, d)", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "readonlyMember", data: { name: "mod" }, column: 29 }] + }, + { + code: "import * as mod from 'mod'; (Object?.defineProperty)(mod, key, d)", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "readonlyMember", data: { name: "mod" }, column: 29 }] + }, + { + code: "import * as mod from 'mod'; delete mod?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "readonlyMember", data: { name: "mod" }, column: 29 }] } ] }); diff --git a/tests/lib/rules/no-invalid-this.js b/tests/lib/rules/no-invalid-this.js index 3662a836766..3eb4e7b0960 100644 --- a/tests/lib/rules/no-invalid-this.js +++ b/tests/lib/rules/no-invalid-this.js @@ -366,6 +366,12 @@ const patterns = [ invalid: [USE_STRICT, IMPLIED_STRICT, MODULES], errors }, + { + code: "obj.foo = (function() { return function() { console.log(this); z(x => console.log(x, this)); }; })?.();", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, // Class Instance Methods. { @@ -421,6 +427,24 @@ const patterns = [ valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], invalid: [] }, + { + code: "var foo = function() { console.log(this); z(x => console.log(x, this)); }?.bind(obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "var foo = (function() { console.log(this); z(x => console.log(x, this)); }?.bind)(obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "var foo = function() { console.log(this); z(x => console.log(x, this)); }.bind?.(obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, // Array methods. { @@ -534,6 +558,30 @@ const patterns = [ valid: [NORMAL], invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] }, + { + code: "Array?.from([], function() { console.log(this); z(x => console.log(x, this)); }, obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "foo?.every(function() { console.log(this); z(x => console.log(x, this)); }, obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "(Array?.from)([], function() { console.log(this); z(x => console.log(x, this)); }, obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "(foo?.every)(function() { console.log(this); z(x => console.log(x, this)); }, obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, // @this tag. { diff --git a/tests/lib/rules/no-irregular-whitespace.js b/tests/lib/rules/no-irregular-whitespace.js index 7852d5e5c27..4055d360150 100644 --- a/tests/lib/rules/no-irregular-whitespace.js +++ b/tests/lib/rules/no-irregular-whitespace.js @@ -540,6 +540,335 @@ ruleTester.run("no-irregular-whitespace", rule, { column: 14 } ] + }, + + // full location tests + { + code: "var foo = \u000B bar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 12 + } + ] + }, + { + code: "var foo =\u000Bbar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 10, + endLine: 1, + endColumn: 11 + } + ] + }, + { + code: "var foo = \u000B\u000B bar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 13 + } + ] + }, + { + code: "var foo = \u000B\u000C bar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 13 + } + ] + }, + { + code: "var foo = \u000B \u000B bar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 12 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 13, + endLine: 1, + endColumn: 14 + } + ] + }, + { + code: "var foo = \u000Bbar\u000B;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 12 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 15, + endLine: 1, + endColumn: 16 + } + ] + }, + { + code: "\u000B", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 1, + endLine: 1, + endColumn: 2 + } + ] + }, + { + code: "\u00A0\u2002\u2003", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 1, + endLine: 1, + endColumn: 4 + } + ] + }, + { + code: "var foo = \u000B\nbar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 12 + } + ] + }, + { + code: "var foo =\u000B\n\u000Bbar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 10, + endLine: 1, + endColumn: 11 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 2, + column: 1, + endLine: 2, + endColumn: 2 + } + ] + }, + { + code: "var foo = \u000C\u000B\n\u000C\u000B\u000Cbar\n;\u000B\u000C\n", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 13 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 2, + column: 1, + endLine: 2, + endColumn: 4 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 3, + column: 2, + endLine: 3, + endColumn: 4 + } + ] + }, + { + code: "var foo = \u2028bar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 2, + endColumn: 1 + } + ] + }, + { + code: "var foo =\u2029 bar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 10, + endLine: 2, + endColumn: 1 + } + ] + }, + { + code: "var foo = bar;\u2028", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 15, + endLine: 2, + endColumn: 1 + } + ] + }, + { + code: "\u2029", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 1, + endLine: 2, + endColumn: 1 + } + ] + }, + { + code: "foo\u2028\u2028", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 4, + endLine: 2, + endColumn: 1 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 2, + column: 1, + endLine: 3, + endColumn: 1 + } + ] + }, + { + code: "foo\u2029\u2028", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 4, + endLine: 2, + endColumn: 1 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 2, + column: 1, + endLine: 3, + endColumn: 1 + } + ] + }, + { + code: "foo\u2028\n\u2028", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 4, + endLine: 2, + endColumn: 1 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 3, + column: 1, + endLine: 4, + endColumn: 1 + } + ] + }, + { + code: "foo\u000B\u2028\u000B", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 4, + endLine: 1, + endColumn: 5 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 5, + endLine: 2, + endColumn: 1 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 2, + column: 1, + endLine: 2, + endColumn: 2 + } + ] } ] }); diff --git a/tests/lib/rules/no-magic-numbers.js b/tests/lib/rules/no-magic-numbers.js index 9683dfc704a..bbdf8ca984f 100644 --- a/tests/lib/rules/no-magic-numbers.js +++ b/tests/lib/rules/no-magic-numbers.js @@ -238,6 +238,25 @@ ruleTester.run("no-magic-numbers", rule, { code: "var one, two; [one = 1, two = 2] = []", options: [{ ignoreDefaultValues: true }], env: { es6: true } + }, + + // Optional chaining + { + code: "var x = parseInt?.(y, 10);", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var x = Number?.parseInt(y, 10);", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var x = (Number?.parseInt)(y, 10);", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "foo?.[777]", + options: [{ ignoreArrayIndexes: true }], + parserOptions: { ecmaVersion: 2020 } } ], invalid: [ diff --git a/tests/lib/rules/no-obj-calls.js b/tests/lib/rules/no-obj-calls.js index 4c21d9be4c8..6270e7a7280 100644 --- a/tests/lib/rules/no-obj-calls.js +++ b/tests/lib/rules/no-obj-calls.js @@ -315,6 +315,18 @@ ruleTester.run("no-obj-calls", rule, { code: "var foo = window.Atomics; new foo;", env: { es2020: true, browser: true }, errors: [{ messageId: "unexpectedRefCall", data: { name: "foo", ref: "Atomics" }, type: "NewExpression" }] + }, + + // Optional chaining + { + code: "var x = globalThis?.Reflect();", + env: { es2020: true }, + errors: [{ messageId: "unexpectedCall", data: { name: "Reflect" }, type: "CallExpression" }] + }, + { + code: "var x = (globalThis?.Reflect)();", + env: { es2020: true }, + errors: [{ messageId: "unexpectedCall", data: { name: "Reflect" }, type: "CallExpression" }] } ] }); diff --git a/tests/lib/rules/no-prototype-builtins.js b/tests/lib/rules/no-prototype-builtins.js index e4f0fa30f78..8f57545bef9 100644 --- a/tests/lib/rules/no-prototype-builtins.js +++ b/tests/lib/rules/no-prototype-builtins.js @@ -94,6 +94,18 @@ const invalid = [ data: { prop: "isPrototypeOf" }, type: "CallExpression" }] + }, + + // Optional chaining + { + code: "foo?.hasOwnProperty('bar')", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }] + }, + { + code: "(foo?.hasOwnProperty)('bar')", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }] } ]; diff --git a/tests/lib/rules/no-restricted-syntax.js b/tests/lib/rules/no-restricted-syntax.js index c8ba48fb8ec..cf8bc412366 100644 --- a/tests/lib/rules/no-restricted-syntax.js +++ b/tests/lib/rules/no-restricted-syntax.js @@ -128,6 +128,35 @@ ruleTester.run("no-restricted-syntax", rule, { code: "console.log(/a/i);", options: ["Literal[regex.flags=/./]"], errors: [{ messageId: "restrictedSyntax", data: { message: "Using 'Literal[regex.flags=/./]' is not allowed." }, type: "Literal" }] + }, + + // Optional chaining + { + code: "var foo = foo?.bar?.();", + options: ["ChainExpression"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "restrictedSyntax", data: { message: "Using 'ChainExpression' is not allowed." }, type: "ChainExpression" }] + }, + { + code: "var foo = foo?.bar?.();", + options: ["[optional=true]"], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "restrictedSyntax", data: { message: "Using '[optional=true]' is not allowed." }, type: "CallExpression" }, + { messageId: "restrictedSyntax", data: { message: "Using '[optional=true]' is not allowed." }, type: "MemberExpression" } + ] } + + /* + * TODO(mysticatea): fix https://github.com/estools/esquery/issues/110 + * { + * code: "a?.b", + * options: [":nth-child(1)"], + * parserOptions: { ecmaVersion: 2020 }, + * errors: [ + * { messageId: "restrictedSyntax", data: { message: "Using ':nth-child(1)' is not allowed." }, type: "ExpressionStatement" } + * ] + * } + */ ] }); diff --git a/tests/lib/rules/no-self-assign.js b/tests/lib/rules/no-self-assign.js index 5149dac65b2..5a9bb6fcfed 100644 --- a/tests/lib/rules/no-self-assign.js +++ b/tests/lib/rules/no-self-assign.js @@ -135,6 +135,18 @@ ruleTester.run("no-self-assign", rule, { options: [{ props: true }], errors: [{ messageId: "selfAssignment", data: { name: "this.x" } }] }, - { code: "a['/(?0)/'] = a[/(?0)/]", options: [{ props: true }], parserOptions: { ecmaVersion: 2018 }, errors: [{ messageId: "selfAssignment", data: { name: "a[/(?0)/]" } }] } + { code: "a['/(?0)/'] = a[/(?0)/]", options: [{ props: true }], parserOptions: { ecmaVersion: 2018 }, errors: [{ messageId: "selfAssignment", data: { name: "a[/(?0)/]" } }] }, + + // Optional chaining + { + code: "(a?.b).c = (a?.b).c", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "selfAssignment", data: { name: "(a?.b).c" } }] + }, + { + code: "a.b = a?.b", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "selfAssignment", data: { name: "a?.b" } }] + } ] }); diff --git a/tests/lib/rules/no-setter-return.js b/tests/lib/rules/no-setter-return.js index b70b4e3f1c0..0c64e8b4811 100644 --- a/tests/lib/rules/no-setter-return.js +++ b/tests/lib/rules/no-setter-return.js @@ -39,7 +39,7 @@ function error(column, type = "ReturnStatement") { // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("no-setter-return", rule, { valid: [ @@ -505,6 +505,18 @@ ruleTester.run("no-setter-return", rule, { { code: "Object.defineProperty(foo, 'bar', { set: function(Object) { return 1; } })", errors: [error()] + }, + + // Optional chaining + { + code: "Object?.defineProperty(foo, 'bar', { set(val) { return 1; } })", + parserOptions: { ecmaVersion: 2020 }, + errors: [error()] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { set(val) { return 1; } })", + parserOptions: { ecmaVersion: 2020 }, + errors: [error()] } ] }); diff --git a/tests/lib/rules/no-throw-literal.js b/tests/lib/rules/no-throw-literal.js index 05002117793..7836cec7837 100644 --- a/tests/lib/rules/no-throw-literal.js +++ b/tests/lib/rules/no-throw-literal.js @@ -38,7 +38,9 @@ ruleTester.run("no-throw-literal", rule, { "throw foo ? 'literal' : new Error();", // ConditionalExpression (alternate) { code: "throw tag `${foo}`;", parserOptions: { ecmaVersion: 6 } }, // TaggedTemplateExpression { code: "function* foo() { var index = 0; throw yield index++; }", parserOptions: { ecmaVersion: 6 } }, // YieldExpression - { code: "async function foo() { throw await bar; }", parserOptions: { ecmaVersion: 8 } } // AwaitExpression + { code: "async function foo() { throw await bar; }", parserOptions: { ecmaVersion: 8 } }, // AwaitExpression + { code: "throw obj?.foo", parserOptions: { ecmaVersion: 2020 } }, // ChainExpression + { code: "throw obj?.foo()", parserOptions: { ecmaVersion: 2020 } } // ChainExpression ], invalid: [ { diff --git a/tests/lib/rules/no-unexpected-multiline.js b/tests/lib/rules/no-unexpected-multiline.js index 29d05a9215b..83c7bf67570 100644 --- a/tests/lib/rules/no-unexpected-multiline.js +++ b/tests/lib/rules/no-unexpected-multiline.js @@ -122,6 +122,24 @@ ruleTester.run("no-unexpected-multiline", rule, { >\`multiline\`; `, parser: require.resolve("../../fixtures/parsers/typescript-parsers/tagged-template-with-generic/tagged-template-with-generic-3") + }, + + // Optional chaining + { + code: "var a = b\n ?.(x || y).doSomething()", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var a = b\n ?.[a, b, c].forEach(doSomething)", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var a = b?.\n (x || y).doSomething()", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var a = b?.\n [a, b, c].forEach(doSomething)", + parserOptions: { ecmaVersion: 2020 } } ], invalid: [ diff --git a/tests/lib/rules/no-unneeded-ternary.js b/tests/lib/rules/no-unneeded-ternary.js index 82479a94cb3..7ad11d2b2e5 100644 --- a/tests/lib/rules/no-unneeded-ternary.js +++ b/tests/lib/rules/no-unneeded-ternary.js @@ -54,7 +54,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 19 + column: 9, + endLine: 1, + endColumn: 31 }] }, { @@ -64,7 +66,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 18 + column: 9, + endLine: 1, + endColumn: 30 }] }, { @@ -74,7 +78,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 25 }] }, { @@ -84,7 +90,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 19 + column: 9, + endLine: 1, + endColumn: 31 }] }, { @@ -94,7 +102,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 18 + column: 9, + endLine: 1, + endColumn: 30 }] }, { @@ -104,7 +114,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 17 + column: 9, + endLine: 1, + endColumn: 29 }] }, { @@ -114,7 +126,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 18 + column: 9, + endLine: 1, + endColumn: 30 }] }, { @@ -124,7 +138,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 21 + column: 9, + endLine: 1, + endColumn: 33 }] }, { @@ -134,7 +150,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 28 + column: 9, + endLine: 1, + endColumn: 40 }] }, { @@ -144,7 +162,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 15 + column: 9, + endLine: 1, + endColumn: 28 }] }, { @@ -154,7 +174,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 17 + column: 9, + endLine: 1, + endColumn: 30 }] }, { @@ -164,7 +186,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 28 + column: 9, + endLine: 1, + endColumn: 40 }] }, { @@ -174,7 +198,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 16 + column: 9, + endLine: 1, + endColumn: 28 }] }, { @@ -193,7 +219,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 4, - column: 38 + column: 30, + endLine: 4, + endColumn: 78 }] }, { @@ -204,7 +232,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 7 + column: 1, + endLine: 1, + endColumn: 30 }] }, { @@ -216,7 +246,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 24 + column: 18, + endLine: 1, + endColumn: 39 }] }, { @@ -227,7 +259,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 15 + column: 9, + endLine: 1, + endColumn: 25 }] }, { @@ -238,7 +272,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 24 + column: 9, + endLine: 1, + endColumn: 66 }] }, { @@ -250,7 +286,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 23 }] }, { @@ -262,7 +300,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 22 }] }, { @@ -274,7 +314,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 25 }] }, { @@ -286,7 +328,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 24 }] }, { @@ -298,7 +342,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 27 }] }, { @@ -310,7 +356,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 18 }] }, { @@ -322,7 +370,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 23 }] }, { @@ -333,7 +383,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 7 + column: 3, + endLine: 1, + endColumn: 12 }] }, { @@ -344,7 +396,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 5 + column: 1, + endLine: 1, + endColumn: 10 }] }, { @@ -355,7 +409,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 15 + column: 9, + endLine: 1, + endColumn: 24 }] }, { @@ -367,7 +423,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 15 + column: 9, + endLine: 1, + endColumn: 27 }] } ] diff --git a/tests/lib/rules/no-unused-expressions.js b/tests/lib/rules/no-unused-expressions.js index 8ef2028c662..1674629c90c 100644 --- a/tests/lib/rules/no-unused-expressions.js +++ b/tests/lib/rules/no-unused-expressions.js @@ -74,6 +74,14 @@ ruleTester.run("no-unused-expressions", rule, { { code: "import(\"foo\")", parserOptions: { ecmaVersion: 11 } + }, + { + code: "func?.(\"foo\")", + parserOptions: { ecmaVersion: 11 } + }, + { + code: "obj?.foo(\"bar\")", + parserOptions: { ecmaVersion: 11 } } ], invalid: [ @@ -127,6 +135,23 @@ ruleTester.run("no-unused-expressions", rule, { options: [{ allowTaggedTemplates: false }], parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "unusedExpression" }] + }, + + // Optional chaining + { + code: "obj?.foo", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unusedExpression", type: "ExpressionStatement" }] + }, + { + code: "obj?.foo.bar", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unusedExpression", type: "ExpressionStatement" }] + }, + { + code: "obj?.foo().bar", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unusedExpression", type: "ExpressionStatement" }] } ] }); diff --git a/tests/lib/rules/no-useless-call.js b/tests/lib/rules/no-useless-call.js index 8713ee92c4d..528f2f1b129 100644 --- a/tests/lib/rules/no-useless-call.js +++ b/tests/lib/rules/no-useless-call.js @@ -44,7 +44,13 @@ ruleTester.run("no-useless-call", rule, { "foo.call();", "obj.foo.call();", "foo.apply();", - "obj.foo.apply();" + "obj.foo.apply();", + + // Optional chaining + { + code: "obj?.foo.bar.call(obj.foo, 1, 2);", + parserOptions: { ecmaVersion: 2020 } + } ], invalid: [ @@ -170,6 +176,86 @@ ruleTester.run("no-useless-call", rule, { data: { name: "apply" }, type: "CallExpression" }] + }, + + // Optional chaining + { + code: "foo.call?.(undefined, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unnecessaryCall", data: { name: "call" } }] + }, + { + code: "foo?.call(undefined, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unnecessaryCall", data: { name: "call" } }] + }, + { + code: "(foo?.call)(undefined, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unnecessaryCall", data: { name: "call" } }] + }, + { + code: "obj.foo.call?.(obj, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "obj?.foo.call(obj, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "(obj?.foo).call(obj, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "(obj?.foo.call)(obj, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "obj?.foo.bar.call(obj?.foo, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "(obj?.foo).bar.call(obj?.foo, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "obj.foo?.bar.call(obj.foo, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] } ] }); diff --git a/tests/lib/rules/no-whitespace-before-property.js b/tests/lib/rules/no-whitespace-before-property.js index d10b30e20e8..31d50449583 100644 --- a/tests/lib/rules/no-whitespace-before-property.js +++ b/tests/lib/rules/no-whitespace-before-property.js @@ -99,7 +99,45 @@ ruleTester.run("no-whitespace-before-property", rule, { "foo[bar.baz('qux')]", "foo[(bar.baz() + 0) + qux]", "foo['bar ' + 1 + ' baz']", - "5['toExponential']()" + "5['toExponential']()", + + // Optional chaining + { + code: "obj?.prop", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "( obj )?.prop", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n ?.prop", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\n prop", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[key]", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "( obj )?.[ key ]", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n ?.[key]", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\n [key]", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n ?.\n [key]", + parserOptions: { ecmaVersion: 2020 } + } ], invalid: [ @@ -859,6 +897,56 @@ ruleTester.run("no-whitespace-before-property", rule, { messageId: "unexpectedWhitespace", data: { propName: "toExponential" } }] + }, + + // Optional chaining + { + code: "obj?. prop", + output: "obj?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] + }, + { + code: "obj ?.prop", + output: "obj?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] + }, + { + code: "obj?. [key]", + output: "obj?.[key]", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "key" } }] + }, + { + code: "obj ?.[key]", + output: "obj?.[key]", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "key" } }] + }, + { + code: "5 ?. prop", + output: "5?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] + }, + { + code: "5 ?. [key]", + output: "5?.[key]", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "key" } }] + }, + { + code: "obj/* comment */?. prop", + output: null, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] + }, + { + code: "obj ?./* comment */prop", + output: null, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] } ] }); diff --git a/tests/lib/rules/object-curly-newline.js b/tests/lib/rules/object-curly-newline.js index ad3b45218b3..86e95609920 100644 --- a/tests/lib/rules/object-curly-newline.js +++ b/tests/lib/rules/object-curly-newline.js @@ -607,8 +607,20 @@ ruleTester.run("object-curly-newline", rule, { ].join("\n"), options: ["always"], errors: [ - { line: 1, column: 9, messageId: "expectedLinebreakAfterOpeningBrace" }, - { line: 1, column: 14, messageId: "expectedLinebreakBeforeClosingBrace" } + { + line: 1, + column: 9, + endLine: 1, + endColumn: 10, + messageId: "expectedLinebreakAfterOpeningBrace" + }, + { + line: 1, + column: 14, + endLine: 1, + endColumn: 15, + messageId: "expectedLinebreakBeforeClosingBrace" + } ] }, { @@ -717,8 +729,20 @@ ruleTester.run("object-curly-newline", rule, { ].join("\n"), options: ["never"], errors: [ - { line: 1, column: 9, messageId: "unexpectedLinebreakAfterOpeningBrace" }, - { line: 3, column: 1, messageId: "unexpectedLinebreakBeforeClosingBrace" } + { + line: 1, + column: 9, + endLine: 1, + endColumn: 10, + messageId: "unexpectedLinebreakAfterOpeningBrace" + }, + { + line: 3, + column: 1, + endLine: 3, + endColumn: 2, + messageId: "unexpectedLinebreakBeforeClosingBrace" + } ] }, { diff --git a/tests/lib/rules/operator-assignment.js b/tests/lib/rules/operator-assignment.js index f690f7eb7f9..ca7dfce6911 100644 --- a/tests/lib/rules/operator-assignment.js +++ b/tests/lib/rules/operator-assignment.js @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/operator-assignment"), // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 7 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); const EXPECTED_OPERATOR_ASSIGNMENT = [{ messageId: "replaced", type: "AssignmentExpression" }]; const UNEXPECTED_OPERATOR_ASSIGNMENT = [{ messageId: "unexpected", type: "AssignmentExpression" }]; @@ -398,6 +398,20 @@ ruleTester.run("operator-assignment", rule, { output: "foo= foo+(+bar===baz)", // tokens cannot be adjacent, but the right side will be parenthesised options: ["never"], errors: UNEXPECTED_OPERATOR_ASSIGNMENT - }] + }, + + // Optional chaining + { + code: "(obj?.a).b = (obj?.a).b + y", + output: null, + errors: EXPECTED_OPERATOR_ASSIGNMENT + }, + { + code: "obj.a = obj?.a + b", + output: null, + errors: EXPECTED_OPERATOR_ASSIGNMENT + } + + ] }); diff --git a/tests/lib/rules/padding-line-between-statements.js b/tests/lib/rules/padding-line-between-statements.js index e553610e353..931cdd24ccb 100644 --- a/tests/lib/rules/padding-line-between-statements.js +++ b/tests/lib/rules/padding-line-between-statements.js @@ -3632,6 +3632,22 @@ ruleTester.run("padding-line-between-statements", rule, { errors: [{ messageId: "expectedBlankLine" }] }, + // Optional chaining + { + code: "(function(){\n})?.()\nvar a = 2;", + output: "(function(){\n})?.()\n\nvar a = 2;", + options: [{ blankLine: "always", prev: "iife", next: "*" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedBlankLine" }] + }, + { + code: "void (function(){\n})?.()\nvar a = 2;", + output: "void (function(){\n})?.()\n\nvar a = 2;", + options: [{ blankLine: "always", prev: "iife", next: "*" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedBlankLine" }] + }, + //---------------------------------------------------------------------- // import //---------------------------------------------------------------------- diff --git a/tests/lib/rules/prefer-arrow-callback.js b/tests/lib/rules/prefer-arrow-callback.js index cb380de251d..cb46c4a5021 100644 --- a/tests/lib/rules/prefer-arrow-callback.js +++ b/tests/lib/rules/prefer-arrow-callback.js @@ -21,7 +21,7 @@ const errors = [{ type: "FunctionExpression" }]; -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-arrow-callback", rule, { valid: [ @@ -40,7 +40,9 @@ ruleTester.run("prefer-arrow-callback", rule, { "foo(function bar() { arguments; }.bind(this));", "foo(function bar() { new.target; });", "foo(function bar() { new.target; }.bind(this));", - "foo(function bar() { this; }.bind(this, somethingElse));" + "foo(function bar() { this; }.bind(this, somethingElse));", + "foo((function() {}).bind.bar)", + "foo((function() { this.bar(); }).bind(obj).bind(this))" ], invalid: [ { @@ -165,13 +167,43 @@ ruleTester.run("prefer-arrow-callback", rule, { { code: "qux(async function (foo = 1, bar = 2, baz = 3) { return baz; })", output: "qux(async (foo = 1, bar = 2, baz = 3) => { return baz; })", - parserOptions: { ecmaVersion: 8 }, errors }, { code: "qux(async function (foo = 1, bar = 2, baz = 3) { return this; }.bind(this))", output: "qux(async (foo = 1, bar = 2, baz = 3) => { return this; })", - parserOptions: { ecmaVersion: 8 }, + errors + }, + { + code: "foo((bar || function() {}).bind(this))", + output: null, + errors + }, + { + code: "foo(function() {}.bind(this).bind(obj))", + output: "foo((() => {}).bind(obj))", + errors + }, + + // Optional chaining + { + code: "foo?.(function() {});", + output: "foo?.(() => {});", + errors + }, + { + code: "foo?.(function() { return this; }.bind(this));", + output: "foo?.(() => { return this; });", + errors + }, + { + code: "foo(function() { return this; }?.bind(this));", + output: "foo(() => { return this; });", + errors + }, + { + code: "foo((function() { return this; }?.bind)(this));", + output: null, errors } ] diff --git a/tests/lib/rules/prefer-destructuring.js b/tests/lib/rules/prefer-destructuring.js index 6f1b6e3a1be..c3d9c65706d 100644 --- a/tests/lib/rules/prefer-destructuring.js +++ b/tests/lib/rules/prefer-destructuring.js @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/prefer-destructuring"), // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-destructuring", rule, { valid: [ @@ -141,7 +141,11 @@ ruleTester.run("prefer-destructuring", rule, { { code: "var {bar} = object.foo;", options: [{ object: true }] - } + }, + + // Optional chaining + "var foo = array?.[0];", // because the fixed code can throw TypeError. + "var foo = object?.foo;" ], invalid: [ diff --git a/tests/lib/rules/prefer-exponentiation-operator.js b/tests/lib/rules/prefer-exponentiation-operator.js index 42bf8ec047d..da147b021fd 100644 --- a/tests/lib/rules/prefer-exponentiation-operator.js +++ b/tests/lib/rules/prefer-exponentiation-operator.js @@ -40,7 +40,7 @@ function invalid(code, output) { // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-exponentiation-operator", rule, { valid: [ @@ -346,6 +346,13 @@ ruleTester.run("prefer-exponentiation-operator", rule, { invalid("Math.pow(a, b/**/)", null), invalid("Math.pow(a, b//\n)", null), invalid("Math.pow(a, b)/* comment */;", "a**b/* comment */;"), - invalid("Math.pow(a, b)// comment\n;", "a**b// comment\n;") + invalid("Math.pow(a, b)// comment\n;", "a**b// comment\n;"), + + // Optional chaining + invalid("Math.pow?.(a, b)", "a**b"), + invalid("Math?.pow(a, b)", "a**b"), + invalid("Math?.pow?.(a, b)", "a**b"), + invalid("(Math?.pow)(a, b)", "a**b"), + invalid("(Math?.pow)?.(a, b)", "a**b") ] }); diff --git a/tests/lib/rules/prefer-numeric-literals.js b/tests/lib/rules/prefer-numeric-literals.js index d8ce4117140..d5d5751a6e3 100644 --- a/tests/lib/rules/prefer-numeric-literals.js +++ b/tests/lib/rules/prefer-numeric-literals.js @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/prefer-numeric-literals"), // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-numeric-literals", rule, { valid: [ @@ -318,6 +318,33 @@ ruleTester.run("prefer-numeric-literals", rule, { code: "parseInt('11', 2)//comment\n;", output: "0b11//comment\n;", errors: 1 + }, + + // Optional chaining + { + code: "parseInt?.(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of parseInt()." }] + }, + { + code: "Number?.parseInt(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] + }, + { + code: "Number?.parseInt?.(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] + }, + { + code: "(Number?.parseInt)(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] + }, + { + code: "(Number?.parseInt)?.(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] } ] }); diff --git a/tests/lib/rules/prefer-promise-reject-errors.js b/tests/lib/rules/prefer-promise-reject-errors.js index 012e44ce7a5..de8f4c72524 100644 --- a/tests/lib/rules/prefer-promise-reject-errors.js +++ b/tests/lib/rules/prefer-promise-reject-errors.js @@ -16,7 +16,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 8 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-promise-reject-errors", rule, { @@ -42,7 +42,11 @@ ruleTester.run("prefer-promise-reject-errors", rule, { { code: "new Promise(function(resolve, reject) { reject() })", options: [{ allowEmptyReject: true }] - } + }, + + // Optional chaining + "Promise.reject(obj?.foo)", + "Promise.reject(obj?.foo())" ], invalid: [ @@ -87,7 +91,14 @@ ruleTester.run("prefer-promise-reject-errors", rule, { "new Promise((foo, arguments) => arguments(5))", "new Promise(function({}, reject) { reject(5) })", "new Promise(({}, reject) => reject(5))", - "new Promise((resolve, reject, somethingElse = reject(5)) => {})" + "new Promise((resolve, reject, somethingElse = reject(5)) => {})", + + // Optional chaining + "Promise.reject?.(5)", + "Promise?.reject(5)", + "Promise?.reject?.(5)", + "(Promise?.reject)(5)", + "(Promise?.reject)?.(5)" ].map(invalidCase => { const errors = { errors: [{ messageId: "rejectAnError", type: "CallExpression" }] }; diff --git a/tests/lib/rules/prefer-regex-literals.js b/tests/lib/rules/prefer-regex-literals.js index 65979de83f9..0ddaa8dae3d 100644 --- a/tests/lib/rules/prefer-regex-literals.js +++ b/tests/lib/rules/prefer-regex-literals.js @@ -16,13 +16,14 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-regex-literals", rule, { valid: [ "/abc/", "/abc/g", + // considered as dynamic "new RegExp(pattern)", "RegExp(pattern, 'g')", @@ -41,6 +42,26 @@ ruleTester.run("prefer-regex-literals", rule, { "new RegExp(String.raw`a${''}c`);", "new RegExp('a' + 'b')", "RegExp(1)", + "new RegExp(/a/, 'u');", + "new RegExp(/a/);", + { + code: "new RegExp(/a/, flags);", + options: [{ disallowRedundantWrapping: true }] + }, + { + code: "new RegExp(/a/, `u${flags}`);", + options: [{ disallowRedundantWrapping: true }] + }, + + // redundant wrapping is allowed + { + code: "new RegExp(/a/);", + options: [{}] + }, + { + code: "new RegExp(/a/);", + options: [{ disallowRedundantWrapping: false }] + }, // invalid number of arguments "new RegExp;", @@ -52,6 +73,10 @@ ruleTester.run("prefer-regex-literals", rule, { "RegExp(`a`, `g`, `b`);", "new RegExp(String.raw`a`, String.raw`g`, String.raw`b`);", "RegExp(String.raw`a`, String.raw`g`, String.raw`b`);", + { + code: "new RegExp(/a/, 'u', 'foo');", + options: [{ disallowRedundantWrapping: true }] + }, // not String.raw`` "new RegExp(String`a`);", @@ -196,6 +221,33 @@ ruleTester.run("prefer-regex-literals", rule, { code: "globalThis.RegExp('a');", env: { es2020: true }, errors: [{ messageId: "unexpectedRegExp", type: "CallExpression" }] + }, + + { + code: "new RegExp(/a/);", + options: [{ disallowRedundantWrapping: true }], + errors: [{ messageId: "unexpectedRedundantRegExp", type: "NewExpression", line: 1, column: 1 }] + }, + { + code: "new RegExp(/a/, 'u');", + options: [{ disallowRedundantWrapping: true }], + errors: [{ messageId: "unexpectedRedundantRegExpWithFlags", type: "NewExpression", line: 1, column: 1 }] + }, + { + code: "new RegExp(/a/, `u`);", + options: [{ disallowRedundantWrapping: true }], + errors: [{ messageId: "unexpectedRedundantRegExpWithFlags", type: "NewExpression", line: 1, column: 1 }] + }, + { + code: "new RegExp('a');", + options: [{ disallowRedundantWrapping: true }], + errors: [{ messageId: "unexpectedRegExp", type: "NewExpression", line: 1, column: 1 }] + }, + + // Optional chaining + { + code: "new RegExp((String?.raw)`a`);", + errors: [{ messageId: "unexpectedRegExp" }] } ] }); diff --git a/tests/lib/rules/prefer-spread.js b/tests/lib/rules/prefer-spread.js index 580d7faeba9..7f48d845f1b 100644 --- a/tests/lib/rules/prefer-spread.js +++ b/tests/lib/rules/prefer-spread.js @@ -18,7 +18,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); const errors = [{ messageId: "preferSpread", type: "CallExpression" }]; -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-spread", rule, { valid: [ @@ -39,7 +39,11 @@ ruleTester.run("prefer-spread", rule, { // ignores incomplete things. "foo.apply();", "obj.foo.apply();", - "obj.foo.apply(obj, ...args)" + "obj.foo.apply(obj, ...args)", + + // Optional chaining + "(a?.b).c.foo.apply(a?.b.c, args);", + "a?.b.c.foo.apply((a?.b).c, args);" ], invalid: [ { @@ -73,6 +77,44 @@ ruleTester.run("prefer-spread", rule, { { code: "[].concat.apply([\n/*empty*/\n], args);", errors + }, + + // Optional chaining + { + code: "foo.apply?.(undefined, args);", + errors + }, + { + code: "foo?.apply(undefined, args);", + errors + }, + { + code: "foo?.apply?.(undefined, args);", + errors + }, + { + code: "(foo?.apply)(undefined, args);", + errors + }, + { + code: "(foo?.apply)?.(undefined, args);", + errors + }, + { + code: "(obj?.foo).apply(obj, args);", + errors + }, + { + code: "a?.b.c.foo.apply(a?.b.c, args);", + errors + }, + { + code: "(a?.b.c).foo.apply(a?.b.c, args);", + errors + }, + { + code: "(a?.b).c.foo.apply((a?.b).c, args);", + errors } ] }); diff --git a/tests/lib/rules/radix.js b/tests/lib/rules/radix.js index 2766e80b597..52801c3d183 100644 --- a/tests/lib/rules/radix.js +++ b/tests/lib/rules/radix.js @@ -185,6 +185,28 @@ ruleTester.run("radix", rule, { messageId: "redundantRadix", type: "CallExpression" }] + }, + + // Optional chaining + { + code: "parseInt?.(\"10\");", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missingRadix" }] + }, + { + code: "Number.parseInt?.(\"10\");", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missingRadix" }] + }, + { + code: "Number?.parseInt(\"10\");", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missingRadix" }] + }, + { + code: "(Number?.parseInt)(\"10\");", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missingRadix" }] } ] }); diff --git a/tests/lib/rules/sort-imports.js b/tests/lib/rules/sort-imports.js index 51f3ee3a804..b5da5018919 100644 --- a/tests/lib/rules/sort-imports.js +++ b/tests/lib/rules/sort-imports.js @@ -101,7 +101,45 @@ ruleTester.run("sort-imports", rule, { }, // https://github.com/eslint/eslint/issues/5305 - "import React, {Component} from 'react';" + "import React, {Component} from 'react';", + + // allowSeparatedGroups + { + code: "import b from 'b';\n\nimport a from 'a';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import a from 'a';\n\nimport 'b';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import { b } from 'b';\n\n\nimport { a } from 'a';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import b from 'b';\n// comment\nimport a from 'a';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import b from 'b';\nfoo();\nimport a from 'a';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import { b } from 'b';/*\n comment \n*/import { a } from 'a';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import b from\n'b';\n\nimport\n a from 'a';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import c from 'c';\n\nimport a from 'a';\nimport b from 'b';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import c from 'c';\n\nimport b from 'b';\n\nimport a from 'a';", + options: [{ allowSeparatedGroups: true }] + } ], invalid: [ { @@ -287,6 +325,125 @@ ruleTester.run("sort-imports", rule, { data: { memberName: "qux" }, type: "ImportSpecifier" }] + }, + + // allowSeparatedGroups + { + code: "import b from 'b';\nimport a from 'a';", + output: null, + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b from 'b';\nimport a from 'a';", + output: null, + options: [{}], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b from 'b';\nimport a from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b from 'b';import a from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b from 'b'; /* comment */ import a from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b from 'b'; // comment\nimport a from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b from 'b'; // comment 1\n/* comment 2 */import a from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import { b } from 'b'; /* comment line 1 \n comment line 2 */ import { a } from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b\nfrom 'b'; import a\nfrom 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import { b } from \n'b'; /* comment */ import\n { a } from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import { b } from \n'b';\nimport\n { a } from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import c from 'c';\n\nimport b from 'b';\nimport a from 'a';", + output: null, + options: [{ allowSeparatedGroups: true }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration", + line: 4 + }] + }, + { + code: "import b from 'b';\n\nimport { c, a } from 'c';", + output: "import b from 'b';\n\nimport { a, c } from 'c';", + options: [{ allowSeparatedGroups: true }], + errors: [{ + messageId: "sortMembersAlphabetically", + type: "ImportSpecifier" + }] } ] }); diff --git a/tests/lib/rules/use-isnan.js b/tests/lib/rules/use-isnan.js index aa1b1746fcd..c5a4b6bd686 100644 --- a/tests/lib/rules/use-isnan.js +++ b/tests/lib/rules/use-isnan.js @@ -385,6 +385,24 @@ ruleTester.run("use-isnan", rule, { code: "foo.bar.lastIndexOf(NaN)", options: [{ enforceForIndexOf: true }], errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }] + }, + { + code: "foo.indexOf?.(NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + }, + { + code: "foo?.indexOf(NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + }, + { + code: "(foo?.indexOf)(NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] } ] }); diff --git a/tests/lib/rules/utils/ast-utils.js b/tests/lib/rules/utils/ast-utils.js index 683e5ed2a6e..e3790f50f3f 100644 --- a/tests/lib/rules/utils/ast-utils.js +++ b/tests/lib/rules/utils/ast-utils.js @@ -10,6 +10,7 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, + util = require("util"), espree = require("espree"), astUtils = require("../../../../lib/rules/utils/ast-utils"), { Linter } = require("../../../../lib/linter"), @@ -478,6 +479,28 @@ describe("ast-utils", () => { assert.strictEqual(astUtils.getStaticStringValue(ast.body[0].expression), expectedResults[key]); }); }); + + it("should return text of regex literal even if it's not supported natively.", () => { + const node = { + type: "Literal", + value: null, + regex: { pattern: "(?:)", flags: "u" } + }; + const expectedText = "/(?:)/u"; + + assert.strictEqual(astUtils.getStaticStringValue(node), expectedText); + }); + + it("should return text of bigint literal even if it's not supported natively.", () => { + const node = { + type: "Literal", + value: null, + bigint: "100n" + }; + const expectedText = "100n"; + + assert.strictEqual(astUtils.getStaticStringValue(node), expectedText); + }); }); describe("getStaticPropertyName", () => { @@ -1411,6 +1434,134 @@ describe("ast-utils", () => { }); }); + describe("equalLiteralValue", () => { + describe("should return true if two regex values are same, even if it's not supported natively.", () => { + const patterns = [ + { + nodeA: { + type: "Literal", + value: /(?:)/u, + regex: { pattern: "(?:)", flags: "u" } + }, + nodeB: { + type: "Literal", + value: /(?:)/u, + regex: { pattern: "(?:)", flags: "u" } + }, + expected: true + }, + { + nodeA: { + type: "Literal", + value: null, + regex: { pattern: "(?:)", flags: "u" } + }, + nodeB: { + type: "Literal", + value: null, + regex: { pattern: "(?:)", flags: "u" } + }, + expected: true + }, + { + nodeA: { + type: "Literal", + value: null, + regex: { pattern: "(?:)", flags: "u" } + }, + nodeB: { + type: "Literal", + value: /(?:)/, // eslint-disable-line require-unicode-regexp + regex: { pattern: "(?:)", flags: "" } + }, + expected: false + }, + { + nodeA: { + type: "Literal", + value: null, + regex: { pattern: "(?:a)", flags: "u" } + }, + nodeB: { + type: "Literal", + value: null, + regex: { pattern: "(?:b)", flags: "u" } + }, + expected: false + } + ]; + + for (const { nodeA, nodeB, expected } of patterns) { + it(`should return ${expected} if it compared ${util.format("%o", nodeA)} and ${util.format("%o", nodeB)}`, () => { + assert.strictEqual(astUtils.equalLiteralValue(nodeA, nodeB), expected); + }); + } + }); + + describe("should return true if two bigint values are same, even if it's not supported natively.", () => { + const patterns = [ + { + nodeA: { + type: "Literal", + value: null, + bigint: "1" + }, + nodeB: { + type: "Literal", + value: null, + bigint: "1" + }, + expected: true + }, + { + nodeA: { + type: "Literal", + value: null, + bigint: "1" + }, + nodeB: { + type: "Literal", + value: null, + bigint: "2" + }, + expected: false + }, + { + nodeA: { + type: "Literal", + value: 1n, + bigint: "1" + }, + nodeB: { + type: "Literal", + value: 1n, + bigint: "1" + }, + expected: true + }, + { + nodeA: { + type: "Literal", + value: 1n, + bigint: "1" + }, + nodeB: { + type: "Literal", + value: 2n, + bigint: "2" + }, + expected: false + } + ]; + + for (const { nodeA, nodeB, expected } of patterns) { + it(`should return ${expected} if it compared ${util.format("%o", nodeA)} and ${util.format("%o", nodeB)}`, () => { + assert.strictEqual(astUtils.equalLiteralValue(nodeA, nodeB), expected); + }); + } + }); + }); + describe("hasOctalEscapeSequence", () => { /* eslint-disable quote-props */ diff --git a/tests/lib/rules/wrap-iife.js b/tests/lib/rules/wrap-iife.js index d18948f429a..035e265da40 100644 --- a/tests/lib/rules/wrap-iife.js +++ b/tests/lib/rules/wrap-iife.js @@ -603,6 +603,36 @@ ruleTester.run("wrap-iife", rule, { options: ["inside", { functionPrototypeMethods: true }], parserOptions: { ecmaVersion: 2020 }, errors: [wrapExpressionError] + }, + + // Optional chaining + { + code: "window.bar = function() { return 3; }.call?.(this, arg1);", + output: "window.bar = (function() { return 3; }).call?.(this, arg1);", + options: ["inside", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapInvocationError] + }, + { + code: "window.bar = function() { return 3; }?.call(this, arg1);", + output: "window.bar = (function() { return 3; })?.call(this, arg1);", + options: ["inside", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapInvocationError] + }, + { + code: "window.bar = (function() { return 3; }?.call)(this, arg1);", + output: "window.bar = ((function() { return 3; })?.call)(this, arg1);", + options: ["inside", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapInvocationError] + }, + { + code: "new (function () {} ?.());", + output: "new ((function () {}) ?.());", + options: ["inside"], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapExpressionError] } ] }); diff --git a/tests/lib/rules/yoda.js b/tests/lib/rules/yoda.js index 43449401873..0fd2c3ee78f 100644 --- a/tests/lib/rules/yoda.js +++ b/tests/lib/rules/yoda.js @@ -288,6 +288,11 @@ ruleTester.run("yoda", rule, { code: "if('a' <= x && x < MAX) {}", options: ["never", { exceptRange: true }] }, + { + code: "if (0 <= obj?.a && obj?.a < 1) {}", + options: ["never", { exceptRange: true }], + parserOptions: { ecmaVersion: 2020 } + }, // onlyEquality { diff --git a/tests/lib/shared/config-ops.js b/tests/lib/shared/config-ops.js index 73f34155405..519d5a2fa0d 100644 --- a/tests/lib/shared/config-ops.js +++ b/tests/lib/shared/config-ops.js @@ -9,7 +9,6 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, - leche = require("leche"), util = require("util"), ConfigOps = require("../../../lib/shared/config-ops"); @@ -199,7 +198,7 @@ describe("ConfigOps", () => { describe("isError()", () => { - leche.withData([ + [ ["error", true], ["Error", true], [2, true], @@ -209,7 +208,7 @@ describe("ConfigOps", () => { [["error", "foo"], true], [["eRror", "bar"], true], [[2, "baz"], true] - ], (input, expected) => { + ].forEach(([input, expected]) => { it(`should return ${expected}when passed ${input}`, () => { const result = ConfigOps.isErrorSeverity(input); diff --git a/tests/lib/shared/naming.js b/tests/lib/shared/naming.js index 0a868a872ed..84bec232431 100644 --- a/tests/lib/shared/naming.js +++ b/tests/lib/shared/naming.js @@ -8,7 +8,6 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, - leche = require("leche"), naming = require("../../../lib/shared/naming"); //------------------------------------------------------------------------------ @@ -18,7 +17,7 @@ const assert = require("chai").assert, describe("naming", () => { describe("normalizePackageName()", () => { - leche.withData([ + [ ["foo", "eslint-config-foo"], ["eslint-config-foo", "eslint-config-foo"], ["@z/foo", "@z/eslint-config-foo"], @@ -26,7 +25,7 @@ describe("naming", () => { ["@z\\foo\\bar.js", "@z/eslint-config-foo/bar.js"], ["@z/eslint-config", "@z/eslint-config"], ["@z/eslint-config-foo", "@z/eslint-config-foo"] - ], (input, expected) => { + ].forEach(([input, expected]) => { it(`should return ${expected} when passed ${input}`, () => { const result = naming.normalizePackageName(input, "eslint-config"); @@ -38,14 +37,14 @@ describe("naming", () => { describe("getShorthandName()", () => { - leche.withData([ + [ ["foo", "foo"], ["eslint-config-foo", "foo"], ["@z", "@z"], ["@z/eslint-config", "@z"], ["@z/foo", "@z/foo"], ["@z/eslint-config-foo", "@z/foo"] - ], (input, expected) => { + ].forEach(([input, expected]) => { it(`should return ${expected} when passed ${input}`, () => { const result = naming.getShorthandName(input, "eslint-config"); diff --git a/tests/lib/source-code/source-code.js b/tests/lib/source-code/source-code.js index e89f9a3283d..c44d4a2e434 100644 --- a/tests/lib/source-code/source-code.js +++ b/tests/lib/source-code/source-code.js @@ -13,7 +13,6 @@ const fs = require("fs"), assert = require("chai").assert, espree = require("espree"), sinon = require("sinon"), - leche = require("leche"), { Linter } = require("../../../lib/linter"), SourceCode = require("../../../lib/source-code/source-code"), astUtils = require("../../../lib/shared/ast-utils"); @@ -1791,13 +1790,13 @@ describe("SourceCode", () => { describe("isSpaceBetween()", () => { describe("should return true when there is at least one whitespace character between two tokens", () => { - leche.withData([ + [ ["let foo", true], ["let foo", true], ["let /**/ foo", true], ["let/**/foo", false], ["let/*\n*/foo", false] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -1829,7 +1828,7 @@ describe("SourceCode", () => { }); }); - leche.withData([ + [ ["a+b", false], ["a +b", true], ["a/**/+b", false], @@ -1861,7 +1860,7 @@ describe("SourceCode", () => { ["a/* */+ ` /*\n*/ `/* */+c", true], ["a/* */+` /*\n*/ ` /* */+c", true], ["a/* */+ ` /*\n*/ ` /* */+c", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -1895,7 +1894,7 @@ describe("SourceCode", () => { }); describe("should return true when there is at least one whitespace character between a token and a node", () => { - leche.withData([ + [ [";let foo = bar", false], [";/**/let foo = bar", false], [";/* */let foo = bar", false], @@ -1923,7 +1922,7 @@ describe("SourceCode", () => { [";/* */\nlet foo = bar", true], [";\n/**/\nlet foo = bar", true], [";\n/* */\nlet foo = bar", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -1957,7 +1956,7 @@ describe("SourceCode", () => { }); describe("should return true when there is at least one whitespace character between a node and a token", () => { - leche.withData([ + [ ["let foo = bar;;", false], ["let foo = bar;;;", false], ["let foo = 1; let bar = 2;;", true], @@ -1985,7 +1984,7 @@ describe("SourceCode", () => { ["let foo = bar;/* */\n;", true], ["let foo = bar;\n/**/\n;", true], ["let foo = bar;\n/* */\n;", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2019,7 +2018,7 @@ describe("SourceCode", () => { }); describe("should return true when there is at least one whitespace character between two nodes", () => { - leche.withData([ + [ ["let foo = bar;let baz = qux;", false], ["let foo = bar;/**/let baz = qux;", false], ["let foo = bar;/* */let baz = qux;", false], @@ -2045,7 +2044,7 @@ describe("SourceCode", () => { ["let foo = bar;\n/**/\nlet baz = qux;", true], ["let foo = bar;\n/* */\nlet baz = qux;", true], ["let foo = 1;let foo2 = 2; let foo3 = 3;", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2142,9 +2141,9 @@ describe("SourceCode", () => { }); describe("should return false either of the arguments' location is inside the other one", () => { - leche.withData([ + [ ["let foo = bar;", false] - ], (code, expected) => { + ].forEach(([code, expected]) => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), sourceCode = new SourceCode(code, ast); @@ -2187,13 +2186,13 @@ describe("SourceCode", () => { describe("isSpaceBetweenTokens()", () => { describe("should return true when there is at least one whitespace character between two tokens", () => { - leche.withData([ + [ ["let foo", true], ["let foo", true], ["let /**/ foo", true], ["let/**/foo", false], ["let/*\n*/foo", false] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2225,7 +2224,7 @@ describe("SourceCode", () => { }); }); - leche.withData([ + [ ["a+b", false], ["a +b", true], ["a/**/+b", false], @@ -2257,7 +2256,7 @@ describe("SourceCode", () => { ["a/* */+ ` /*\n*/ `/* */+c", true], ["a/* */+` /*\n*/ ` /* */+c", true], ["a/* */+ ` /*\n*/ ` /* */+c", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2291,7 +2290,7 @@ describe("SourceCode", () => { }); describe("should return true when there is at least one whitespace character between a token and a node", () => { - leche.withData([ + [ [";let foo = bar", false], [";/**/let foo = bar", false], [";/* */let foo = bar", false], @@ -2319,7 +2318,7 @@ describe("SourceCode", () => { [";/* */\nlet foo = bar", true], [";\n/**/\nlet foo = bar", true], [";\n/* */\nlet foo = bar", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2353,7 +2352,7 @@ describe("SourceCode", () => { }); describe("should return true when there is at least one whitespace character between a node and a token", () => { - leche.withData([ + [ ["let foo = bar;;", false], ["let foo = bar;;;", false], ["let foo = 1; let bar = 2;;", true], @@ -2381,7 +2380,7 @@ describe("SourceCode", () => { ["let foo = bar;/* */\n;", true], ["let foo = bar;\n/**/\n;", true], ["let foo = bar;\n/* */\n;", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2415,7 +2414,7 @@ describe("SourceCode", () => { }); describe("should return true when there is at least one whitespace character between two nodes", () => { - leche.withData([ + [ ["let foo = bar;let baz = qux;", false], ["let foo = bar;/**/let baz = qux;", false], ["let foo = bar;/* */let baz = qux;", false], @@ -2441,7 +2440,7 @@ describe("SourceCode", () => { ["let foo = bar;\n/**/\nlet baz = qux;", true], ["let foo = bar;\n/* */\nlet baz = qux;", true], ["let foo = 1;let foo2 = 2; let foo3 = 3;", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2538,9 +2537,9 @@ describe("SourceCode", () => { }); describe("should return false either of the arguments' location is inside the other one", () => { - leche.withData([ + [ ["let foo = bar;", false] - ], (code, expected) => { + ].forEach(([code, expected]) => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), sourceCode = new SourceCode(code, ast); diff --git a/tests/tools/internal-rules/consistent-docs-url.js b/tests/tools/internal-rules/consistent-docs-url.js index 3c61a181f39..4b3d3a05b67 100644 --- a/tests/tools/internal-rules/consistent-docs-url.js +++ b/tests/tools/internal-rules/consistent-docs-url.js @@ -55,7 +55,7 @@ ruleTester.run("consistent-docs-url", rule, { "};" ].join("\n"), errors: [{ - message: "Rule is missing a meta.docs property", + messageId: "missingMetaDocs", line: 2, column: 5 }] @@ -73,7 +73,7 @@ ruleTester.run("consistent-docs-url", rule, { "};" ].join("\n"), errors: [{ - message: "Rule is missing a meta.docs.url property", + messageId: "missingMetaDocsUrl", line: 3, column: 9 }] @@ -92,7 +92,11 @@ ruleTester.run("consistent-docs-url", rule, { "};" ].join("\n"), errors: [{ - message: "Incorrect url. Expected \"https://eslint.org/docs/rules/\" but got \"http://example.com/wrong-url\"", + messageId: "incorrectUrl", + data: { + expected: "https://eslint.org/docs/rules/", + url: "http://example.com/wrong-url" + }, line: 4, column: 18 }] diff --git a/tools/code-sample-minimizer.js b/tools/code-sample-minimizer.js index 512b692fe74..c68f4582050 100644 --- a/tools/code-sample-minimizer.js +++ b/tools/code-sample-minimizer.js @@ -52,7 +52,7 @@ function reduceBadExampleSize({ comment: true, eslintVisitorKeys: true, eslintScopeManager: true, - ecmaVersion: 2018, + ecmaVersion: espree.latestEcmaVersion, sourceType: "script" }) }, diff --git a/tools/eslint-fuzzer.js b/tools/eslint-fuzzer.js index ff77f44f6e1..4104b8516b2 100644 --- a/tools/eslint-fuzzer.js +++ b/tools/eslint-fuzzer.js @@ -129,7 +129,10 @@ function fuzz(options) { const text = codeGenerator({ sourceType }); const config = { rules: lodash.mapValues(ruleConfigs, lodash.sample), - parserOptions: { sourceType, ecmaVersion: 2020 } + parserOptions: { + sourceType, + ecmaVersion: espree.latestEcmaVersion + } }; let autofixResult; diff --git a/tools/internal-rules/consistent-docs-url.js b/tools/internal-rules/consistent-docs-url.js index 1a52bd59280..052fe55f28a 100644 --- a/tools/internal-rules/consistent-docs-url.js +++ b/tools/internal-rules/consistent-docs-url.js @@ -55,7 +55,7 @@ function checkMetaDocsUrl(context, exportsNode) { if (!metaDocs) { context.report({ node: metaProperty, - message: "Rule is missing a meta.docs property" + messageId: "missingMetaDocs" }); return; } @@ -63,7 +63,7 @@ function checkMetaDocsUrl(context, exportsNode) { if (!metaDocsUrl) { context.report({ node: metaDocs, - message: "Rule is missing a meta.docs.url property" + messageId: "missingMetaDocsUrl" }); return; } @@ -75,7 +75,8 @@ function checkMetaDocsUrl(context, exportsNode) { if (url !== expected) { context.report({ node: metaDocsUrl.value, - message: `Incorrect url. Expected "${expected}" but got "${url}"` + messageId: "incorrectUrl", + data: { expected, url } }); } @@ -93,7 +94,12 @@ module.exports = { recommended: false }, type: "suggestion", - schema: [] + schema: [], + messages: { + missingMetaDocs: "Rule is missing a meta.docs property.", + missingMetaDocsUrl: "Rule is missing a meta.docs.url property.", + incorrectUrl: 'Incorrect url. Expected "{{ expected }}" but got "{{ url }}".' + } }, create(context) { diff --git a/tools/rule-types.json b/tools/rule-types.json index 6f2e6a834b3..84700de70d0 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -44,6 +44,7 @@ "guard-for-in": "suggestion", "handle-callback-err": "suggestion", "id-blacklist": "suggestion", + "id-denylist": "suggestion", "id-length": "suggestion", "id-match": "suggestion", "implicit-arrow-linebreak": "layout", @@ -280,4 +281,4 @@ "wrap-regex": "layout", "yield-star-spacing": "layout", "yoda": "suggestion" -} \ No newline at end of file +} diff --git a/webpack.config.js b/webpack.config.js index 29d60cb4d27..2b56290b346 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -43,7 +43,7 @@ module.exports = { ] }, resolve: { - mainFields: ["main", "module"] + mainFields: ["browser", "main", "module"] }, stats: "errors-only" };