diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md index ef8c5101cd96..a614d6b2f5c7 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -1,5 +1,5 @@ --- -name: Bug report +name: "\U0001F41E Bug report" about: Report an issue with ESLint or rules bundled with ESLint --- @@ -51,4 +51,10 @@ about: Report an issue with ESLint or rules bundled with ESLint **What did you expect to happen?** -**What actually happened? Please include the actual, raw output from ESLint.** \ No newline at end of file + +**What actually happened? Please include the actual, raw output from ESLint.** + + +**Are you willing to submit a pull request to fix this bug?** + + diff --git a/.github/ISSUE_TEMPLATE/CHANGE.md b/.github/ISSUE_TEMPLATE/CHANGE.md index 14cff91b0e06..3fe2fe241a7b 100644 --- a/.github/ISSUE_TEMPLATE/CHANGE.md +++ b/.github/ISSUE_TEMPLATE/CHANGE.md @@ -1,5 +1,5 @@ --- -name: Non-rule change request +name: "\U0001F4DD Non-rule change request" about: Request a change that is not a bug fix, rule change, or new rule --- @@ -19,7 +19,12 @@ about: Request a change that is not a bug fix, rule change, or new rule **The version of ESLint you are using.** + **The problem you want to solve.** + **Your take on the correct solution to problem.** + +**Are you willing to submit a pull request to implement this change?** + diff --git a/.github/ISSUE_TEMPLATE/NEW_RULE.md b/.github/ISSUE_TEMPLATE/NEW_RULE.md index 454b28e4090e..8dfb93ba281b 100644 --- a/.github/ISSUE_TEMPLATE/NEW_RULE.md +++ b/.github/ISSUE_TEMPLATE/NEW_RULE.md @@ -1,5 +1,5 @@ --- -name: New rule proposal +/name: "\U0001F680 New rule proposal" about: Propose a new rule to be added to ESLint --- @@ -35,3 +35,6 @@ about: Propose a new rule to be added to ESLint **Why should this rule be included in ESLint (instead of a plugin)?** + +**Are you willing to submit a pull request to implement this rule?** + diff --git a/.github/ISSUE_TEMPLATE/QUESTION.md b/.github/ISSUE_TEMPLATE/QUESTION.md new file mode 100644 index 000000000000..ba96b41044c9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/QUESTION.md @@ -0,0 +1,22 @@ +--- +name: "⛔ Question" +about: Please go to https://gitter.im/eslint/eslint + +--- + + diff --git a/.github/ISSUE_TEMPLATE/RULE_CHANGE.md b/.github/ISSUE_TEMPLATE/RULE_CHANGE.md index 7b0c6537971d..9ecb2d002b5d 100644 --- a/.github/ISSUE_TEMPLATE/RULE_CHANGE.md +++ b/.github/ISSUE_TEMPLATE/RULE_CHANGE.md @@ -1,5 +1,5 @@ --- -name: Rule change request +name: "\U0001F4DD Rule change request" about: Request a change to an existing rule --- @@ -34,3 +34,4 @@ about: Request a change to an existing rule **What will the rule do after it's changed?** +**Are you willing to submit a pull request to implement this change?** \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/SECURITY.md b/.github/ISSUE_TEMPLATE/SECURITY.md new file mode 100644 index 000000000000..8cc33ec6ecaf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/SECURITY.md @@ -0,0 +1,19 @@ +--- +name: "⛔ Security issue" +about: Please file security issues at https://hackerone.com/eslint + +--- + + diff --git a/.gitignore b/.gitignore index eae2b21d1ab1..d2c98d47bd91 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ versions.json /packages/**/node_modules /.vscode .sublimelinterrc +.eslint-release-info.json diff --git a/.travis.yml b/.travis.yml index ff3e956a7025..22decd59f58d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ node_js: - "8" - "9" - "10" + - "11" sudo: false branches: only: diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a372690c2f4..f91359ef4c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,50 @@ +v5.8.0 - October 26, 2018 + +* 9152417 Fix: deprecation warning in RuleTester using Node v11 (#11009) (Teddy Katz) +* e349a03 Docs: Update issue templates to ask for PRs (#11012) (Nicholas C. Zakas) +* 3d88b38 Chore: avoid using legacy report API in no-irregular-whitespace (#11013) (Teddy Katz) +* 5a31a92 Build: compile espree's deps to ES5 when generating site (fixes #11014) (#11015) (Teddy Katz) +* 3943635 Update: Create Linter.version API (fixes #9271) (#11010) (Nicholas C. Zakas) +* a940cf4 Docs: Mention version for config glob patterns (fixes #8793) (Nicholas C. Zakas) +* 6e1c530 Build: run tests on Node 11 (#11008) (Teddy Katz) +* 58ff359 Docs: add instructions for npm 2FA (refs #10631) (#10992) (Teddy Katz) +* 2f87bb3 Upgrade: eslint-release@1.0.0 (refs #10631) (#10991) (Teddy Katz) +* 57ef0fd Fix: prefer-const when using destructuring assign (fixes #8308) (#10924) (Nicholas C. Zakas) +* 577cbf1 Chore: Add typescript-specific edge case tests to space-infix-ops (#10986) (Bence Dányi) +* d45b184 Chore: Using deconstruction assignment for shelljs (#10974) (ZYSzys) + +v5.7.0 - October 12, 2018 + +* 6cb63fd Update: Add iife to padding-line-between-statements (fixes #10853) (#10916) (Kevin Partington) +* 5fd1bda Update: no-tabs allowIndentationTabs option (fixes #10256) (#10925) (Kevin Partington) +* d12be69 Fix: no-extra-bind No autofix if arg may have side effect (fixes #10846) (#10918) (Kevin Partington) +* 847372f Fix: no-unused-vars false pos. with markVariableAsUsed (fixes #10952) (#10954) (Roy Sutton) +* 4132de7 Chore: Simplify space-infix-ops (#10935) (Bence Dányi) +* 543edfa Fix: Fix error with one-var (fixes #10937) (#10938) (Justin Krup) +* 95c4cb1 Docs: Fix typo for no-unsafe-finally (#10945) (Sergio Santoro) +* 5fe0e1a Fix: no-invalid-regexp disallows \ at end of pattern (fixes #10861) (#10920) (Toru Nagashima) +* f85547a Docs: Add 'When Not To Use' section to space-infix-ops (#10931) (Bence Dányi) +* 3dccac4 Docs: Update working-with-parsers link (#10929) (Azeem Bande-Ali) +* 557a8bb Docs: Remove old note about caching, add a new one (fixes #10739) (#10913) (Zac) +* fe8111a Chore: Add more test cases to space-infix-ops (#10936) (Bence Dányi) +* 066f7e0 Update: camelcase rule ignoreList added (#10783) (Julien Martin) +* 70bde69 Upgrade: table to version 5 (#10903) (Rouven Weßling) +* 2e52bca Chore: Update issue templates (#10900) (Nicholas C. Zakas) + +v5.6.1 - September 28, 2018 + +* 9b26bdb Fix: avoid exponential require-atomic-updates traversal (fixes #10893) (#10894) (Teddy Katz) +* 9432b10 Fix: make separateRequires work in consecutive mode (fixes #10784) (#10886) (Pig Fang) +* e51868d Upgrade: debug@4 (fixes #10854) (#10887) (薛定谔的猫) +* d3f3994 Docs: add information about reporting security issues (#10889) (Teddy Katz) +* cc458f4 Build: fix failing tests on master (#10890) (Teddy Katz) +* a6ebfd3 Docs: clarify defaultAssignment option, fix no-unneeded-ternary examples (#10874) (CoffeeTableEspresso) +* 9d52541 Fix: Remove duplicate error message on crash (fixes #8964) (#10865) (Nicholas C. Zakas) +* 4eb9a49 Docs: Update quotes.md (#10862) (The Jared Wilcurt) +* 9159e9b Docs: Update complexity.md (#10867) (Szymon Przybylski) +* 14f4e46 Docs: Use Linter instead of linter in Nodejs API page (#10864) (Nicholas C. Zakas) +* b3e3cb1 Chore: Update debug log name to match filename (#10863) (Nicholas C. Zakas) + v5.6.0 - September 14, 2018 * c5b688e Update: Added generators option to func-names (fixes #9511) (#10697) (Oscar Barrett) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 411e7fb523b4..d0db77ccfe63 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,8 @@ Before filing an issue, please be sure to read the guidelines for what you're re * [Proposing a Rule Change](https://eslint.org/docs/developer-guide/contributing/rule-changes) * [Request a Change](https://eslint.org/docs/developer-guide/contributing/changes) +To report a security vulnerability in ESLint, please use our [HackerOne program](https://hackerone.com/eslint). + ## Contributing Code Please sign our [Contributor License Agreement](https://cla.js.foundation/eslint/eslint) and read over the [Pull Request Guidelines](https://eslint.org/docs/developer-guide/contributing/pull-requests). diff --git a/Makefile.js b/Makefile.js index 3c514af096c8..473ffdf58872 100644 --- a/Makefile.js +++ b/Makefile.js @@ -23,23 +23,11 @@ const lodash = require("lodash"), os = require("os"), path = require("path"), semver = require("semver"), - shell = require("shelljs"), ejs = require("ejs"), loadPerf = require("load-perf"), yaml = require("js-yaml"); -const cat = shell.cat; -const cd = shell.cd; -const cp = shell.cp; -const echo = shell.echo; -const exec = shell.exec; -const exit = shell.exit; -const find = shell.find; -const ls = shell.ls; -const mkdir = shell.mkdir; -const pwd = shell.pwd; -const rm = shell.rm; -const test = shell.test; +const { cat, cd, cp, echo, exec, exit, find, ls, mkdir, pwd, rm, test } = require("shelljs"); //------------------------------------------------------------------------------ // Settings @@ -162,10 +150,11 @@ function execSilent(cmd) { /** * Generates a release blog post for eslint.org * @param {Object} releaseInfo The release metadata. + * @param {string} [prereleaseMajorVersion] If this is a prerelease, the next major version after this prerelease * @returns {void} * @private */ -function generateBlogPost(releaseInfo) { +function generateBlogPost(releaseInfo, prereleaseMajorVersion) { const ruleList = ls("lib/rules") // Strip the .js extension @@ -178,7 +167,7 @@ function generateBlogPost(releaseInfo) { */ .sort((ruleA, ruleB) => ruleB.length - ruleA.length); - const renderContext = Object.assign({ prereleaseMajorVersion: null, ruleList }, releaseInfo); + const renderContext = Object.assign({ prereleaseMajorVersion, ruleList }, releaseInfo); const output = ejs.render(cat("./templates/blogpost.md.ejs"), renderContext), now = new Date(), @@ -262,11 +251,12 @@ function generateRuleIndexPage(basedir) { } /** - * Commits the changes in the site and publishes them to GitHub. - * @param {string} [tag] The string to tag the commit with. + * Creates a git commit and tag in an adjacent `eslint.github.io` repository, without pushing it to + * the remote. This assumes that the repository has already been modified somehow (e.g. by adding a blogpost). + * @param {string} [tag] The string to tag the commit with * @returns {void} */ -function publishSite(tag) { +function commitSiteToGit(tag) { const currentDir = pwd(); cd(SITE_DIR); @@ -278,39 +268,53 @@ function publishSite(tag) { } exec("git fetch origin && git rebase origin/master"); - exec("git push origin master --tags"); cd(currentDir); } /** - * Creates a release version tag and pushes to origin. - * @param {boolean} [ciRelease] Set to true to indicate this is a CI release. + * Publishes the changes in an adjacent `eslint.github.io` repository to the remote. The + * site should already have local commits (e.g. from running `commitSiteToGit`). * @returns {void} */ -function release(ciRelease) { +function publishSite() { + const currentDir = pwd(); + + cd(SITE_DIR); + exec("git push origin master --tags"); + cd(currentDir); +} - const releaseInfo = ReleaseOps.release(null, ciRelease); +/** + * Updates the changelog, bumps the version number in package.json, creates a local git commit and tag, + * and generates the site in an adjacent `eslint.github.io` folder. + * @returns {void} + */ +function generateRelease() { + ReleaseOps.generateRelease(); + const releaseInfo = JSON.parse(cat(".eslint-release-info.json")); echo("Generating site"); target.gensite(); generateBlogPost(releaseInfo); - publishSite(`v${releaseInfo.version}`); - echo("Site has been published"); - - echo("Publishing to GitHub"); - ReleaseOps.publishReleaseToGitHub(releaseInfo); + commitSiteToGit(`v${releaseInfo.version}`); } /** - * Creates a prerelease version tag and pushes to origin. + * Updates the changelog, bumps the version number in package.json, creates a local git commit and tag, + * and generates the site in an adjacent `eslint.github.io` folder. * @param {string} prereleaseId The prerelease identifier (alpha, beta, etc.) * @returns {void} */ -function prerelease(prereleaseId) { - - const releaseInfo = ReleaseOps.release(prereleaseId); +function generatePrerelease(prereleaseId) { + ReleaseOps.generateRelease(prereleaseId); + const releaseInfo = JSON.parse(cat(".eslint-release-info.json")); const nextMajor = semver.inc(releaseInfo.version, "major"); + echo("Generating site"); + + // always write docs into the next major directory (so 2.0.0-alpha.0 writes to 2.0.0) + target.gensite(nextMajor); + /* * Premajor release should have identical "next major version". * Preminor and prepatch release will not. @@ -325,21 +329,23 @@ function prerelease(prereleaseId) { * Blog post generation logic needs to be aware of this (as well as * know what the next major version is actually supposed to be). */ - releaseInfo.prereleaseMajorVersion = nextMajor; + generateBlogPost(releaseInfo, nextMajor); + } else { + generateBlogPost(releaseInfo); } - echo("Generating site"); - - // always write docs into the next major directory (so 2.0.0-alpha.0 writes to 2.0.0) - target.gensite(nextMajor); - generateBlogPost(releaseInfo); - publishSite(`v${releaseInfo.version}`); - echo("Site has been published"); - - echo("Publishing to GitHub"); - ReleaseOps.publishReleaseToGitHub(releaseInfo); + commitSiteToGit(`v${releaseInfo.version}`); } +/** + * Publishes a generated release to npm and GitHub, and pushes changes to the adjacent `eslint.github.io` repo + * to remote repo. + * @returns {void} + */ +function publishRelease() { + ReleaseOps.publishRelease(); + publishSite(); +} /** * Splits a command result to separate lines. @@ -843,7 +849,7 @@ target.browserify = function() { exec(`${getBinFile("browserify")} -x espree ${TEMP_DIR}linter.js -o ${BUILD_DIR}eslint.js -s eslint --global-transform [ babelify --presets [ es2015 ] ]`); // 6. Browserify espree - exec(`${getBinFile("browserify")} -r espree -o ${TEMP_DIR}espree.js`); + exec(`${getBinFile("browserify")} -r espree -o ${TEMP_DIR}espree.js --global-transform [ babelify --presets [ es2015 ] ]`); // 7. Concatenate Babel polyfill, Espree, and ESLint files together cat("./node_modules/babel-polyfill/dist/polyfill.js", `${TEMP_DIR}espree.js`, `${BUILD_DIR}eslint.js`).to(`${BUILD_DIR}eslint.js`); @@ -1141,18 +1147,6 @@ target.perf = function() { }); }; -target.release = function() { - release(); -}; - -target.ciRelease = function() { - release(true); -}; - -target.publishsite = function() { - publishSite(); -}; - -target.prerelease = function(args) { - prerelease(args[0]); -}; +target.generateRelease = generateRelease; +target.generatePrerelease = ([prereleaseType]) => generatePrerelease(prereleaseType); +target.publishRelease = publishRelease; diff --git a/bin/eslint.js b/bin/eslint.js index adc07c2e75c3..e51121ec6afe 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -48,7 +48,6 @@ process.once("uncaughtException", err => { console.error(`\nESLint: ${pkg.version}.\n${template(err.messageData || {})}`); } else { - console.error(err.message); console.error(err.stack); } diff --git a/docs/developer-guide/README.md b/docs/developer-guide/README.md index 2070b2da13ea..ea1943099778 100644 --- a/docs/developer-guide/README.md +++ b/docs/developer-guide/README.md @@ -34,7 +34,7 @@ You're finally ready to start working with rules. You may want to fix an existin You've developed library-specific rules for ESLint and you want to share it with the community. You can publish an ESLint plugin on npm. -## Section 6: [Working with Parsers](working-with-parsers.md) +## Section 6: [Working with Custom Parsers](working-with-custom-parsers.md) If you aren't going to use the default parser of ESLint, this section explains about using custom parsers. diff --git a/docs/developer-guide/contributing/README.md b/docs/developer-guide/contributing/README.md index a4e331ef9d63..a22ea5d90a26 100644 --- a/docs/developer-guide/contributing/README.md +++ b/docs/developer-guide/contributing/README.md @@ -28,6 +28,10 @@ Want to make a change to an existing rule? This section explains the process and If you'd like to request a change other than a bug fix or new rule, this section explains that process. +## Reporting a security vulnerability + +To report a security vulnerability in ESLint, please use our [HackerOne program](https://hackerone.com/eslint). + ## [Working on Issues](working-on-issues.md) Have some extra time and want to contribute? This section talks about the process of working on issues. diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 2e3390b9365d..8dbccc7afb19 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -117,7 +117,8 @@ var messages = linter.verify("var foo;", { // or using SourceCode -var linter = require("eslint").linter, +var Linter = require("eslint").Linter, + linter = new Linter(), SourceCode = require("eslint").SourceCode; var code = new SourceCode("var foo = bar;", ast); @@ -290,7 +291,7 @@ linter.defineParser("my-custom-parser", { const results = linter.verify("// some source text", { parser: "my-custom-parser" }); ``` -### Linter#version +### Linter#version/Linter.version Each instance of `Linter` has a `version` property containing the semantic version number of ESLint that the `Linter` instance is from. @@ -301,6 +302,14 @@ const linter = new Linter(); linter.version; // => '4.5.0' ``` +There is also a `Linter.version` property that you can read without instantiating `Linter`: + +```js +const Linter = require("eslint").Linter; + +Linter.version; // => '4.5.0' +``` + ## linter The `eslint.linter` object (deprecated) is an instance of the `Linter` class as defined [above](#linter). `eslint.linter` exists for backwards compatibility, but we do not recommend using it because any mutations to it are shared among every module that uses `eslint`. Instead, please create your own instance of `eslint.Linter`. diff --git a/docs/maintainer-guide/governance.md b/docs/maintainer-guide/governance.md index 15e5a6340b25..81d5908eabc9 100644 --- a/docs/maintainer-guide/governance.md +++ b/docs/maintainer-guide/governance.md @@ -93,7 +93,7 @@ A Committer is invited to become a TSC member by existing TSC members. A nominat 1. Add the GitHub user to the "ESLint TSC" team 1. Set the GitHub user to be have the "Owner" role for the ESLint organization -1. Send welcome email with link to maintainer guide +1. Send a welcome email with a link to the [maintainer guide](./) and the [npm 2FA guide](./npm-2fa). 1. Add the TSC member to the README 1. Invite to the Gitter TSC chatroom 1. Make the TSC member an admin on the ESLint team mailing list diff --git a/docs/maintainer-guide/npm-2fa.md b/docs/maintainer-guide/npm-2fa.md new file mode 100644 index 000000000000..e9a517761fba --- /dev/null +++ b/docs/maintainer-guide/npm-2fa.md @@ -0,0 +1,16 @@ +# npm two-factor authentication + +The `eslint` npm account has two-factor authentication (2FA) enabled. The 2FA secret is distributed using a team on [Keybase](https://keybase.io). Anyone doing a release of a package from the Jenkins server needs to have access to the 2FA secret. + +If you're on ESLint's TSC, you should perform the following steps to obtain the 2FA secret: + +1. Download the [Keybase app](https://keybase.io/download) on a smartphone. +1. Open the app and create an account. +1. From the app, link your Keybase username with your GitHub username. (At the time of writing, the UI for this is to tap the face icon in the bottom-left of the app, then the profile picture in the top-right, then tap "Prove your GitHub" and follow the instructions.) +1. Mention your Keybase username in the team chatroom, and wait for someone to add you to the Keybase team. +1. Download an authenticator app like [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/), if you don't have one installed already. +1. In the Keybase app, navigate to the Keybase filesystem (at the time of writing, the UI for this is to tap the hamburger icon in the bottom-right, then tap "Files") and then navigate to `/team/eslint/auth`. + * If your authenticator app is downloaded on the same device as your Keybase app (this will usually be the case if you're using the Keybase mobile app), then open `npm_2fa_code.txt` and copy the contents to the clipboard. Open your authenticator app, and paste the contents as a new key (by selecting something like "Enter a provided key" or "Enter key manually"). + * If your authenticator app is downloaded on a *different* device from your Keybase app (e.g. if you're using a Keybase desktop app), then open `npm_2fa_code.png` and scan it as a QR code from your authenticator app. + +You should now be able to generate 6-digit 2FA codes for the `eslint` npm account using your authenticator app. diff --git a/docs/maintainer-guide/releases.md b/docs/maintainer-guide/releases.md index e8faab5f63a7..9d00030f3dff 100644 --- a/docs/maintainer-guide/releases.md +++ b/docs/maintainer-guide/releases.md @@ -16,6 +16,8 @@ A two-person release team is assigned to each scheduled release. This two-person The two-person team should seek input from the whole team on the Monday following a release to double-check if a patch release is necessary. +At least one member of the release team needs to have access to [eslint's two-factor authentication for npm](./npm-2fa) in order to do a release. + ## Release Communication Each scheduled release should be associated with a release issue ([example](https://github.com/eslint/eslint/issues/8138)). The release issue is the source of information for the team about the status of a release. Be sure the release issue has the "release" label so that it's easy to find. @@ -30,7 +32,9 @@ On the day of a scheduled release, the release team should follow these steps: * Documentation changes. * Small bug fixes written by a team member. 1. Log into Jenkins and schedule a build for the "ESLint Release" job. -1. Wait for the "ESLint Release" job to complete. +1. Watch the console output of the build on Jenkins. At some point, the build will pause and a link will be produced with an input field for a six-digit 2FA code. +1. Enter the current six-digit 2FA code from your authenticator app. (Also see: [npm-2fa](./npm-2fa)) +1. Continue the build and wait for it to finish. 1. Update the release blog post with a "Highlights" section, including new rules and anything else that's important. 1. Make a release announcement in the public chatroom. 1. Make a release announcement on Twitter. diff --git a/docs/rules/camelcase.md b/docs/rules/camelcase.md index cce3fd92470f..1765f1852c47 100644 --- a/docs/rules/camelcase.md +++ b/docs/rules/camelcase.md @@ -14,6 +14,7 @@ This rule has an object option: * `"properties": "never"` does not check property names * `"ignoreDestructuring": false` (default) enforces camelcase style for destructured identifiers * `"ignoreDestructuring": true` does not check destructured identifiers +* `allow` (`string[]`) list of properties to accept. Accept regex. ### properties: "always" @@ -151,6 +152,30 @@ var { category_id = 1 } = query; var { category_id: category_id } = query; ``` +## allow + +Examples of **correct** code for this rule with the `allow` option: + +```js +/*eslint camelcase: ["error", {allow: ["UNSAFE_componentWillMount"]}]*/ + +function UNSAFE_componentWillMount() { + // ... +} +``` + +```js +/*eslint camelcase: ["error", {allow: ["^UNSAFE_"]}]*/ + +function UNSAFE_componentWillMount() { + // ... +} + +function UNSAFE_componentWillMount() { + // ... +} +``` + ## When Not To Use It If you have established coding standards using a different naming convention (separating words with underscores), turn this rule off. diff --git a/docs/rules/complexity.md b/docs/rules/complexity.md index e70a8989ac1d..3a665491c39f 100644 --- a/docs/rules/complexity.md +++ b/docs/rules/complexity.md @@ -71,7 +71,7 @@ If you can't determine an appropriate complexity limit for your code, then it's ## Further Reading * [Cyclomatic Complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity) -* [Complexity Analysis of JavaScript Code](http://ariya.ofilabs.com/2012/12/complexity-analysis-of-javascript-code.html) +* [Complexity Analysis of JavaScript Code](https://ariya.io/2012/12/complexity-analysis-of-javascript-code) * [More about Complexity in JavaScript](https://craftsmanshipforsoftware.com/2015/05/25/complexity-for-javascript/) * [About Complexity](https://web.archive.org/web/20160808115119/http://jscomplexity.org/complexity) * [Discussion about Complexity in ESLint and more links](https://github.com/eslint/eslint/issues/4808#issuecomment-167795140) diff --git a/docs/rules/no-tabs.md b/docs/rules/no-tabs.md index 691b2f2af2a4..4e02d9a04c63 100644 --- a/docs/rules/no-tabs.md +++ b/docs/rules/no-tabs.md @@ -9,14 +9,14 @@ This rule looks for tabs anywhere inside a file: code, comments or anything else Examples of **incorrect** code for this rule: ```js -var a /t= 2; +var a \t= 2; /** -* /t/t it's a test function +* \t\t it's a test function */ function test(){} -var x = 1; // /t test +var x = 1; // \t test ``` Examples of **correct** code for this rule: @@ -32,9 +32,29 @@ function test(){} var x = 1; // test ``` +### Options + +This rule has an optional object option with the following properties: + +* `allowIndentationTabs` (default: false): If this is set to true, then the rule will not report tabs used for indentation. + +#### allowIndentationTabs + +Examples of **correct** code for this rule with the `allowIndentationTabs: true` option: + +```js +/* eslint no-tabs: ["error", { allowIndentationTabs: true }] */ + +function test() { +\tdoSomething(); +} + +\t// comment with leading indentation tab +``` + ## When Not To Use It -If you have established a standard where having tabs is fine. +If you have established a standard where having tabs is fine, then you can disable this rule. ## Compatibility diff --git a/docs/rules/no-unneeded-ternary.md b/docs/rules/no-unneeded-ternary.md index b2a6b0281ddd..b4caddf58555 100644 --- a/docs/rules/no-unneeded-ternary.md +++ b/docs/rules/no-unneeded-ternary.md @@ -23,10 +23,10 @@ Here is an example: ```js // Bad -var foo = bar ? bar : 1; +foo(bar ? bar : 1); // Good -var foo = bar || 1; +foo(bar || 1); ``` ## Rule Details @@ -41,6 +41,8 @@ Examples of **incorrect** code for this rule: var a = x === 2 ? true : false; var a = x ? true : false; + +var a = f(x ? x : 1); ``` Examples of **correct** code for this rule: @@ -56,7 +58,7 @@ var a = x ? "Yes" : "No"; var a = x ? y : x; -var a = x ? x : 1; +var a = x ? x : 1; // Note that this is only allowed as it on the right hand side of an assignment; this type of ternary is disallowed everywhere else. See defaultAssignment option below for more details. ``` ## Options @@ -68,6 +70,8 @@ This rule has an object option: ### defaultAssignment +The defaultAssignment option allows expressions of the form `x ? x : expr` (where `x` is any identifier and `expr` is any expression) as the right hand side of assignments (but nowhere else). + Examples of additional **incorrect** code for this rule with the `{ "defaultAssignment": false }` option: ```js diff --git a/docs/rules/no-unsafe-finally.md b/docs/rules/no-unsafe-finally.md index 1783e501fc80..fb3bb1ea5beb 100644 --- a/docs/rules/no-unsafe-finally.md +++ b/docs/rules/no-unsafe-finally.md @@ -49,7 +49,7 @@ JavaScript suspends the control flow statements of `try` and `catch` blocks unti // We expect this function to return 0 from try block. (() => { label: try { - return 0; // 1 is returned but suspended until finally block ends + return 0; // 0 is returned but suspended until finally block ends } finally { break label; // It breaks out the try-finally block, before 0 is returned. } diff --git a/docs/rules/padding-line-between-statements.md b/docs/rules/padding-line-between-statements.md index c56259734535..390dada294ef 100644 --- a/docs/rules/padding-line-between-statements.md +++ b/docs/rules/padding-line-between-statements.md @@ -20,9 +20,9 @@ function foo() { ## Rule Details -This rule does nothing if no configuration. +This rule does nothing if no configurations are provided. -A configuration is an object which has 3 properties; `blankLine`, `prev` and `next`. For example, `{ blankLine: "always", prev: "var", next: "return" }` means "it requires one or more blank lines between a variable declaration and a `return` statement." +A configuration is an object which has 3 properties; `blankLine`, `prev` and `next`. For example, `{ blankLine: "always", prev: "var", next: "return" }` means "one or more blank lines are required between a variable declaration and a `return` statement." You can supply any number of configurations. If a statement pair matches multiple configurations, the last matched configuration will be used. ```json @@ -46,11 +46,11 @@ You can supply any number of configurations. If a statement pair matches multipl - `STATEMENT_TYPE` is one of the following, or an array of the following. - `"*"` is wildcard. This matches any statements. - `"block"` is lonely blocks. - - `"block-like"` is block like statements. This matches statements that the last token is the closing brace of blocks; e.g. `{ }`, `if (a) { }`, and `while (a) { }`. + - `"block-like"` is block like statements. This matches statements that the last token is the closing brace of blocks; e.g. `{ }`, `if (a) { }`, and `while (a) { }`. Also matches immediately invoked function expression statements. - `"break"` is `break` statements. - `"case"` is `case` labels. - - `"cjs-export"` is `export` statements of CommonJS; e.g. `module.exports = 0`, `module.exports.foo = 1`, and `exports.foo = 2`. This is the special cases of assignment. - - `"cjs-import"` is `import` statements of CommonJS; e.g. `const foo = require("foo")`. This is the special cases of variable declarations. + - `"cjs-export"` is `export` statements of CommonJS; e.g. `module.exports = 0`, `module.exports.foo = 1`, and `exports.foo = 2`. This is a special case of assignment. + - `"cjs-import"` is `import` statements of CommonJS; e.g. `const foo = require("foo")`. This is a special case of variable declarations. - `"class"` is `class` declarations. - `"const"` is `const` variable declarations. - `"continue"` is `continue` statements. @@ -64,6 +64,7 @@ You can supply any number of configurations. If a statement pair matches multipl - `"for"` is `for` loop families. This matches all statements that the first token is `for` keyword. - `"function"` is function declarations. - `"if"` is `if` statements. + - `"iife"` is immediately invoked function expression statements. This matches calls on a function expression, optionally prefixed with a unary operator. - `"import"` is `import` declarations. - `"let"` is `let` variable declarations. - `"multiline-block-like"` is block like statements. This is the same as `block-like` type, but only if the block is multiline. diff --git a/docs/rules/prefer-const.md b/docs/rules/prefer-const.md index cf86f5b7be0a..3e5a5d0c3feb 100644 --- a/docs/rules/prefer-const.md +++ b/docs/rules/prefer-const.md @@ -79,6 +79,15 @@ for (let i = 0, end = 10; i < end; ++i) { console.log(a); } +// `predicate` is only assigned once but cannot be separately declared as `const` +let predicate; +[object.type, predicate] = foo(); + +// `a` is only assigned once but cannot be separately declared as `const` +let a; +const b = {}; +({ a, c: b.c } = func()); + // suggest to use `no-var` rule. var b = 3; console.log(b); diff --git a/docs/rules/quotes.md b/docs/rules/quotes.md index 02c748c4da8c..25a09fd0dbf5 100644 --- a/docs/rules/quotes.md +++ b/docs/rules/quotes.md @@ -147,6 +147,8 @@ var single = 'single'; var single = `single`; ``` +`{ "allowTemplateLiterals": false }` will not disallow the usage of all template literals. If you want to forbid any instance of template literals, use [no-restricted-syntax](https://eslint.org/docs/rules/no-restricted-syntax) and target the `TemplateLiteral` selector. + ## When Not To Use It If you do not need consistency in your string styles, you can safely disable this rule. diff --git a/docs/rules/space-infix-ops.md b/docs/rules/space-infix-ops.md index f0381877deb2..406e2030cf41 100644 --- a/docs/rules/space-infix-ops.md +++ b/docs/rules/space-infix-ops.md @@ -73,3 +73,7 @@ var {a = 0} = bar; function foo(a = 0) { } ``` + +## When Not To Use It + +You can turn this rule off if you are not concerned with the consistency of spacing around infix operators. diff --git a/docs/user-guide/command-line-interface.md b/docs/user-guide/command-line-interface.md index 7de5a50a1e28..62ecd3c20215 100644 --- a/docs/user-guide/command-line-interface.md +++ b/docs/user-guide/command-line-interface.md @@ -401,7 +401,7 @@ Store the info about processed files in order to only operate on the changed one **Note:** If you run ESLint with `--cache` and then run ESLint without `--cache`, the `.eslintcache` file will be deleted. This is necessary because the results of the lint might change and make `.eslintcache` invalid. If you want to control when the cache file is deleted, then use `--cache-location` to specify an alternate location for the cache file. -**Note:**: As of now, only the results for successfully linted files are stored in the cache. Files which fail linting are not stored in the cache, so they will be linted every time. +**Note:**: Autofixed files are not placed in the cache. Subsequent linting that does not trigger an autofix will place it in the cache. #### `--cache-file` diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index 0db1e4dcf403..3bb64faf871b 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -763,7 +763,7 @@ module.exports = { ## Configuration Based on Glob Patterns -Sometimes a more fine-controlled configuration is necessary, for example if the configuration for files within the same directory has to be different. Therefore you can provide configurations under the `overrides` key that will only apply to files that match specific glob patterns, using the same format you would pass on the command line (e.g., `app/**/*.test.js`). +v4.1.0+. Sometimes a more fine-controlled configuration is necessary, for example if the configuration for files within the same directory has to be different. Therefore you can provide configurations under the `overrides` key that will only apply to files that match specific glob patterns, using the same format you would pass on the command line (e.g., `app/**/*.test.js`). ### How it works diff --git a/lib/linter.js b/lib/linter.js index 8f7bdf9e4c79..b47e6eb1fcfb 100644 --- a/lib/linter.js +++ b/lib/linter.js @@ -888,6 +888,15 @@ module.exports = class Linter { this.environments = new Environments(); } + /** + * Getter for package version. + * @static + * @returns {string} The version from package.json. + */ + static get version() { + return pkg.version; + } + /** * Configuration object for the `verify` API. A JS representation of the eslintrc files. * @typedef {Object} ESLintConfig diff --git a/lib/rules/camelcase.js b/lib/rules/camelcase.js index 3005963ab04c..41040450f942 100644 --- a/lib/rules/camelcase.js +++ b/lib/rules/camelcase.js @@ -27,6 +27,16 @@ module.exports = { }, properties: { enum: ["always", "never"] + }, + allow: { + type: "array", + items: [ + { + type: "string" + } + ], + minItems: 0, + uniqueItems: true } }, additionalProperties: false @@ -40,6 +50,15 @@ module.exports = { create(context) { + const options = context.options[0] || {}; + let properties = options.properties || ""; + const ignoreDestructuring = options.ignoreDestructuring || false; + const allow = options.allow || []; + + if (properties !== "always" && properties !== "never") { + properties = "always"; + } + //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- @@ -60,6 +79,18 @@ module.exports = { return name.indexOf("_") > -1 && name !== name.toUpperCase(); } + /** + * Checks if a string match the ignore list + * @param {string} name The string to check. + * @returns {boolean} if the string is ignored + * @private + */ + function isAllowed(name) { + return allow.findIndex( + entry => name === entry || name.match(new RegExp(entry)) + ) !== -1; + } + /** * Checks if a parent of a node is an ObjectPattern. * @param {ASTNode} node The node to check. @@ -93,14 +124,6 @@ module.exports = { } } - const options = context.options[0] || {}; - let properties = options.properties || ""; - const ignoreDestructuring = options.ignoreDestructuring || false; - - if (properties !== "always" && properties !== "never") { - properties = "always"; - } - return { Identifier(node) { @@ -112,6 +135,11 @@ module.exports = { const name = node.name.replace(/^_+|_+$/g, ""), effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent; + // First, we ignore the node if it match the ignore list + if (isAllowed(name)) { + return; + } + // MemberExpressions get special rules if (node.parent.type === "MemberExpression") { diff --git a/lib/rules/no-extra-bind.js b/lib/rules/no-extra-bind.js index b8376adfc117..6d6cad13e97d 100644 --- a/lib/rules/no-extra-bind.js +++ b/lib/rules/no-extra-bind.js @@ -10,6 +10,12 @@ const astUtils = require("../util/ast-utils"); +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const SIDE_EFFECT_FREE_NODE_TYPES = new Set(["Literal", "Identifier", "ThisExpression", "FunctionExpression"]); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -35,6 +41,18 @@ module.exports = { create(context) { let scopeInfo = null; + /** + * Checks if a node is free of side effects. + * + * This check is stricter than it needs to be, in order to keep the implementation simple. + * + * @param {ASTNode} node A node to check. + * @returns {boolean} True if the node is known to be side-effect free, false otherwise. + */ + function isSideEffectFree(node) { + return SIDE_EFFECT_FREE_NODE_TYPES.has(node.type); + } + /** * Reports a given function node. * @@ -48,6 +66,10 @@ module.exports = { messageId: "unexpected", loc: node.parent.property.loc.start, fix(fixer) { + if (node.parent.parent.arguments.length && !isSideEffectFree(node.parent.parent.arguments[0])) { + return null; + } + const firstTokenToRemove = context.getSourceCode() .getFirstTokenBetween(node.parent.object, node.parent.property, astUtils.isNotClosingParenToken); diff --git a/lib/rules/no-irregular-whitespace.js b/lib/rules/no-irregular-whitespace.js index 729f829a1f71..de6934c635ac 100644 --- a/lib/rules/no-irregular-whitespace.js +++ b/lib/rules/no-irregular-whitespace.js @@ -81,9 +81,7 @@ module.exports = { const locStart = node.loc.start; const locEnd = node.loc.end; - errors = errors.filter(error => { - const errorLoc = error[1]; - + errors = errors.filter(({ loc: 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; @@ -157,7 +155,7 @@ module.exports = { column: match.index }; - errors.push([node, location, "Irregular whitespace not allowed."]); + errors.push({ node, message: "Irregular whitespace not allowed.", loc: location }); } }); } @@ -182,7 +180,7 @@ module.exports = { column: sourceLines[lineIndex].length }; - errors.push([node, location, "Irregular whitespace not allowed."]); + errors.push({ node, message: "Irregular whitespace not allowed.", loc: location }); lastLineIndex = lineIndex; } } @@ -224,9 +222,7 @@ module.exports = { } // If we have any errors remaining report on them - errors.forEach(error => { - context.report(...error); - }); + errors.forEach(error => context.report(error)); }; } else { nodes.Program = noop; diff --git a/lib/rules/no-tabs.js b/lib/rules/no-tabs.js index 08a8fa5b758b..c22a94da38be 100644 --- a/lib/rules/no-tabs.js +++ b/lib/rules/no-tabs.js @@ -8,7 +8,9 @@ //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ -const regex = /\t/; + +const tabRegex = /\t+/g; +const anyNonWhitespaceRegex = /\S/; //------------------------------------------------------------------------------ // Public Interface @@ -22,21 +24,36 @@ module.exports = { recommended: false, url: "https://eslint.org/docs/rules/no-tabs" }, - schema: [] + schema: [{ + type: "object", + properties: { + allowIndentationTabs: { + type: "boolean" + } + }, + additionalProperties: false + }] }, create(context) { + const sourceCode = context.getSourceCode(); + const allowIndentationTabs = context.options && context.options[0] && context.options[0].allowIndentationTabs; + return { Program(node) { - context.getSourceCode().getLines().forEach((line, index) => { - const match = regex.exec(line); + sourceCode.getLines().forEach((line, index) => { + let match; + + while ((match = tabRegex.exec(line)) !== null) { + if (allowIndentationTabs && !anyNonWhitespaceRegex.test(line.slice(0, match.index))) { + continue; + } - if (match) { context.report({ node, loc: { line: index + 1, - column: match.index + 1 + column: match.index }, message: "Unexpected tab character." }); diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js index fff3e33d9235..6f36813aca57 100644 --- a/lib/rules/no-unused-vars.js +++ b/lib/rules/no-unused-vars.js @@ -469,7 +469,7 @@ module.exports = { const posteriorParams = params.slice(params.indexOf(variable) + 1); // If any used parameters occur after this parameter, do not report. - return !posteriorParams.some(v => v.references.length > 0); + return !posteriorParams.some(v => v.references.length > 0 || v.eslintUsed); } /** diff --git a/lib/rules/one-var.js b/lib/rules/one-var.js index 13ab72b04ae5..3efd0f27f7f3 100644 --- a/lib/rules/one-var.js +++ b/lib/rules/one-var.js @@ -314,6 +314,11 @@ module.exports = { function splitDeclarations(declaration) { return fixer => declaration.declarations.map(declarator => { const tokenAfterDeclarator = sourceCode.getTokenAfter(declarator); + + if (tokenAfterDeclarator === null) { + return null; + } + const afterComma = sourceCode.getTokenAfter(tokenAfterDeclarator, { includeComments: true }); if (tokenAfterDeclarator.value !== ",") { @@ -384,8 +389,13 @@ module.exports = { if (nodeIndex > 0) { const previousNode = parent.body[nodeIndex - 1]; const isPreviousNodeDeclaration = previousNode.type === "VariableDeclaration"; + const declarationsWithPrevious = declarations.concat(previousNode.declarations || []); - if (isPreviousNodeDeclaration && previousNode.kind === type) { + if ( + isPreviousNodeDeclaration && + previousNode.kind === type && + !(declarationsWithPrevious.some(isRequire) && !declarationsWithPrevious.every(isRequire)) + ) { const previousDeclCounts = countDeclarations(previousNode.declarations); if (options[type].initialized === MODE_CONSECUTIVE && options[type].uninitialized === MODE_CONSECUTIVE) { diff --git a/lib/rules/padding-line-between-statements.js b/lib/rules/padding-line-between-statements.js index f571e1e2e335..4af85fef015a 100644 --- a/lib/rules/padding-line-between-statements.js +++ b/lib/rules/padding-line-between-statements.js @@ -353,6 +353,9 @@ const StatementTypes = { node.type === "ExpressionStatement" && !isDirectivePrologue(node, sourceCode) }, + iife: { + test: isIIFEStatement + }, "multiline-block-like": { test: (node, sourceCode) => node.loc.start.line !== node.loc.end.line && diff --git a/lib/rules/prefer-const.js b/lib/rules/prefer-const.js index 774fcf064376..8b3bc5e0e436 100644 --- a/lib/rules/prefer-const.js +++ b/lib/rules/prefer-const.js @@ -57,6 +57,7 @@ function canBecomeVariableDeclaration(identifier) { * @returns {boolean} Indicates if the variable is from outer scope or function parameters. */ function isOuterVariableInDestructing(name, initScope) { + if (initScope.through.find(ref => ref.resolved && ref.resolved.name === name)) { return true; } @@ -96,6 +97,54 @@ function getDestructuringHost(reference) { return node; } +/** + * Determines if a destructuring assignment node contains + * any MemberExpression nodes. This is used to determine if a + * variable that is only written once using destructuring can be + * safely converted into a const declaration. + * @param {ASTNode} node The ObjectPattern or ArrayPattern node to check. + * @returns {boolean} True if the destructuring pattern contains + * a MemberExpression, false if not. + */ +function hasMemberExpressionAssignment(node) { + switch (node.type) { + case "ObjectPattern": + return node.properties.some(prop => { + if (prop) { + + /* + * Spread elements have an argument property while + * others have a value property. Because different + * parsers use different node types for spread elements, + * we just check if there is an argument property. + */ + return hasMemberExpressionAssignment(prop.argument || prop.value); + } + + return false; + }); + + case "ArrayPattern": + return node.elements.some(element => { + if (element) { + return hasMemberExpressionAssignment(element); + } + + return false; + }); + + case "AssignmentPattern": + return hasMemberExpressionAssignment(node.left); + + case "MemberExpression": + return true; + + // no default + } + + return false; +} + /** * Gets an identifier node of a given variable. * @@ -148,7 +197,8 @@ function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) { if (destructuringHost !== null && destructuringHost.left !== void 0) { const leftNode = destructuringHost.left; - let hasOuterVariables = false; + let hasOuterVariables = false, + hasNonIdentifiers = false; if (leftNode.type === "ObjectPattern") { const properties = leftNode.properties; @@ -157,16 +207,23 @@ function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) { .filter(prop => prop.value) .map(prop => prop.value.name) .some(name => isOuterVariableInDestructing(name, variable.scope)); + + hasNonIdentifiers = hasMemberExpressionAssignment(leftNode); + } else if (leftNode.type === "ArrayPattern") { const elements = leftNode.elements; hasOuterVariables = elements .map(element => element && element.name) .some(name => isOuterVariableInDestructing(name, variable.scope)); + + hasNonIdentifiers = hasMemberExpressionAssignment(leftNode); } - if (hasOuterVariables) { + + if (hasOuterVariables || hasNonIdentifiers) { return null; } + } writer = reference; @@ -192,9 +249,11 @@ function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) { if (!shouldBeConst) { return null; } + if (isReadBeforeInit) { return variable.defs[0].name; } + return writer.identifier; } @@ -295,7 +354,7 @@ module.exports = { create(context) { const options = context.options[0] || {}; const sourceCode = context.getSourceCode(); - const checkingMixedDestructuring = options.destructuring !== "all"; + const shouldMatchAnyDestructuredVariable = options.destructuring !== "all"; const ignoreReadBeforeAssign = options.ignoreReadBeforeAssign === true; const variables = []; @@ -316,7 +375,7 @@ module.exports = { function checkGroup(nodes) { const nodesToReport = nodes.filter(Boolean); - if (nodes.length && (checkingMixedDestructuring || nodesToReport.length === nodes.length)) { + if (nodes.length && (shouldMatchAnyDestructuredVariable || nodesToReport.length === nodes.length)) { const varDeclParent = findUp(nodes[0], "VariableDeclaration", parentNode => parentNode.type.endsWith("Statement")); const shouldFix = varDeclParent && diff --git a/lib/rules/require-atomic-updates.js b/lib/rules/require-atomic-updates.js index 5d7d7151455e..c6cf0d747740 100644 --- a/lib/rules/require-atomic-updates.js +++ b/lib/rules/require-atomic-updates.js @@ -119,6 +119,66 @@ module.exports = { }); } + const alreadyReportedAssignments = new WeakSet(); + + class AssignmentTrackerState { + constructor({ openAssignmentsWithoutReads = new Set(), openAssignmentsWithReads = new Set() } = {}) { + this.openAssignmentsWithoutReads = openAssignmentsWithoutReads; + this.openAssignmentsWithReads = openAssignmentsWithReads; + } + + copy() { + return new AssignmentTrackerState({ + openAssignmentsWithoutReads: new Set(this.openAssignmentsWithoutReads), + openAssignmentsWithReads: new Set(this.openAssignmentsWithReads) + }); + } + + merge(other) { + const initialAssignmentsWithoutReadsCount = this.openAssignmentsWithoutReads.size; + const initialAssignmentsWithReadsCount = this.openAssignmentsWithReads.size; + + other.openAssignmentsWithoutReads.forEach(assignment => this.openAssignmentsWithoutReads.add(assignment)); + other.openAssignmentsWithReads.forEach(assignment => this.openAssignmentsWithReads.add(assignment)); + + return this.openAssignmentsWithoutReads.size > initialAssignmentsWithoutReadsCount || + this.openAssignmentsWithReads.size > initialAssignmentsWithReadsCount; + } + + enterAssignment(assignmentExpression) { + (assignmentExpression.operator === "=" ? this.openAssignmentsWithoutReads : this.openAssignmentsWithReads).add(assignmentExpression); + } + + exitAssignment(assignmentExpression) { + this.openAssignmentsWithoutReads.delete(assignmentExpression); + this.openAssignmentsWithReads.delete(assignmentExpression); + } + + exitAwaitOrYield(node, surroundingFunction) { + return [...this.openAssignmentsWithReads] + .filter(assignment => !isLocalVariableWithoutEscape(assignment.left, surroundingFunction)) + .forEach(assignment => { + if (!alreadyReportedAssignments.has(assignment)) { + reportAssignment(assignment); + alreadyReportedAssignments.add(assignment); + } + }); + } + + exitIdentifierOrMemberExpression(node) { + [...this.openAssignmentsWithoutReads] + .filter(assignment => ( + assignment.left !== node && + assignment.left.type === node.type && + astUtils.equalTokens(assignment.left, node, sourceCode) + )) + .forEach(assignment => { + this.openAssignmentsWithoutReads.delete(assignment); + this.openAssignmentsWithReads.add(assignment); + }); + } + } + /** * If the control flow graph of a function enters an assignment expression, then does the * both of the following steps in order (possibly with other steps in between) before exiting the @@ -135,54 +195,51 @@ module.exports = { codePathSegment, surroundingFunction, { - seenSegments = new Set(), - openAssignmentsWithoutReads = new Set(), - openAssignmentsWithReads = new Set() + stateBySegmentStart = new WeakMap(), + stateBySegmentEnd = new WeakMap() } = {} ) { - if (seenSegments.has(codePathSegment)) { - - // An AssignmentExpression can't contain loops, so it's not necessary to reenter them with new state. - return; + if (!stateBySegmentStart.has(codePathSegment)) { + stateBySegmentStart.set(codePathSegment, new AssignmentTrackerState()); } + const currentState = stateBySegmentStart.get(codePathSegment).copy(); + expressionsByCodePathSegment.get(codePathSegment).forEach(({ entering, node }) => { if (node.type === "AssignmentExpression") { if (entering) { - (node.operator === "=" ? openAssignmentsWithoutReads : openAssignmentsWithReads).add(node); + currentState.enterAssignment(node); } else { - openAssignmentsWithoutReads.delete(node); - openAssignmentsWithReads.delete(node); + currentState.exitAssignment(node); } } else if (!entering && (node.type === "AwaitExpression" || node.type === "YieldExpression")) { - [...openAssignmentsWithReads] - .filter(assignment => !isLocalVariableWithoutEscape(assignment.left, surroundingFunction)) - .forEach(reportAssignment); - - openAssignmentsWithReads.clear(); + currentState.exitAwaitOrYield(node, surroundingFunction); } else if (!entering && (node.type === "Identifier" || node.type === "MemberExpression")) { - [...openAssignmentsWithoutReads] - .filter(assignment => ( - assignment.left !== node && - assignment.left.type === node.type && - astUtils.equalTokens(assignment.left, node, sourceCode) - )) - .forEach(assignment => { - openAssignmentsWithoutReads.delete(assignment); - openAssignmentsWithReads.add(assignment); - }); + currentState.exitIdentifierOrMemberExpression(node); } }); + stateBySegmentEnd.set(codePathSegment, currentState); + codePathSegment.nextSegments.forEach(nextSegment => { + if (stateBySegmentStart.has(nextSegment)) { + if (!stateBySegmentStart.get(nextSegment).merge(currentState)) { + + /* + * This segment has already been processed with the given set of inputs; + * no need to do it again. After no new state is available to process + * for any control flow segment in the graph, the analysis reaches a fixpoint and + * traversal stops. + */ + return; + } + } else { + stateBySegmentStart.set(nextSegment, currentState.copy()); + } findOutdatedReads( nextSegment, surroundingFunction, - { - seenSegments: new Set(seenSegments).add(codePathSegment), - openAssignmentsWithoutReads: new Set(openAssignmentsWithoutReads), - openAssignmentsWithReads: new Set(openAssignmentsWithReads) - } + { stateBySegmentStart, stateBySegmentEnd } ); }); } diff --git a/lib/rules/space-infix-ops.js b/lib/rules/space-infix-ops.js index 49b64658d190..17b49cc1184c 100644 --- a/lib/rules/space-infix-ops.js +++ b/lib/rules/space-infix-ops.js @@ -34,37 +34,25 @@ module.exports = { create(context) { const int32Hint = context.options[0] ? context.options[0].int32Hint === true : false; - - const OPERATORS = [ - "*", "/", "%", "+", "-", "<<", ">>", ">>>", "<", "<=", ">", ">=", "in", - "instanceof", "==", "!=", "===", "!==", "&", "^", "|", "&&", "||", "=", - "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|=", - "?", ":", ",", "**" - ]; - const sourceCode = context.getSourceCode(); /** * Returns the first token which violates the rule * @param {ASTNode} left - The left node of the main node * @param {ASTNode} right - The right node of the main node + * @param {string} op - The operator of the main node * @returns {Object} The violator token or null * @private */ - function getFirstNonSpacedToken(left, right) { - const tokens = sourceCode.getTokensBetween(left, right, 1); - - for (let i = 1, l = tokens.length - 1; i < l; ++i) { - const op = tokens[i]; - - if ( - (op.type === "Punctuator" || op.type === "Keyword") && - OPERATORS.indexOf(op.value) >= 0 && - (tokens[i - 1].range[1] >= op.range[0] || op.range[1] >= tokens[i + 1].range[0]) - ) { - return op; - } + function getFirstNonSpacedToken(left, right, op) { + const operator = sourceCode.getFirstTokenBetween(left, right, token => token.value === op); + const prev = sourceCode.getTokenBefore(operator); + const next = sourceCode.getTokenAfter(operator); + + if (!sourceCode.isSpaceBetweenTokens(prev, operator) || !sourceCode.isSpaceBetweenTokens(operator, next)) { + return operator; } + return null; } @@ -110,7 +98,10 @@ module.exports = { const leftNode = (node.left.typeAnnotation) ? node.left.typeAnnotation : node.left; const rightNode = node.right; - const nonSpacedNode = getFirstNonSpacedToken(leftNode, rightNode); + // search for = in AssignmentPattern nodes + const operator = node.operator || "="; + + const nonSpacedNode = getFirstNonSpacedToken(leftNode, rightNode, operator); if (nonSpacedNode) { if (!(int32Hint && sourceCode.getText(node).endsWith("|0"))) { @@ -126,8 +117,8 @@ module.exports = { * @private */ function checkConditional(node) { - const nonSpacedConsequesntNode = getFirstNonSpacedToken(node.test, node.consequent); - const nonSpacedAlternateNode = getFirstNonSpacedToken(node.consequent, node.alternate); + const nonSpacedConsequesntNode = getFirstNonSpacedToken(node.test, node.consequent, "?"); + const nonSpacedAlternateNode = getFirstNonSpacedToken(node.consequent, node.alternate, ":"); if (nonSpacedConsequesntNode) { report(node, nonSpacedConsequesntNode); @@ -147,7 +138,7 @@ module.exports = { const rightNode = node.init; if (rightNode) { - const nonSpacedNode = getFirstNonSpacedToken(leftNode, rightNode); + const nonSpacedNode = getFirstNonSpacedToken(leftNode, rightNode, "="); if (nonSpacedNode) { report(node, nonSpacedNode); diff --git a/lib/testers/rule-tester.js b/lib/testers/rule-tester.js index 9fbca945b62e..6d1bba989fde 100644 --- a/lib/testers/rule-tester.js +++ b/lib/testers/rule-tester.js @@ -397,7 +397,7 @@ class RuleTester { */ function assertASTDidntChange(beforeAST, afterAST) { if (!lodash.isEqual(beforeAST, afterAST)) { - assert.fail(null, null, "Rule should not modify AST."); + assert.fail("Rule should not modify AST."); } } @@ -551,7 +551,7 @@ class RuleTester { } else { // Message was an unexpected type - assert.fail(message, null, "Error should be a string, object, or RegExp."); + assert.fail(`Error should be a string, object, or RegExp, but found (${util.inspect(message)})`); } } } diff --git a/lib/util/source-code-fixer.js b/lib/util/source-code-fixer.js index b3354d4d7625..53dc1dc6be73 100644 --- a/lib/util/source-code-fixer.js +++ b/lib/util/source-code-fixer.js @@ -8,7 +8,7 @@ // Requirements //------------------------------------------------------------------------------ -const debug = require("debug")("eslint:text-fixer"); +const debug = require("debug")("eslint:source-code-fixer"); //------------------------------------------------------------------------------ // Helpers diff --git a/package.json b/package.json index 1a37b3c21b74..c2f7626dca64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "5.6.0", + "version": "5.8.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { @@ -11,11 +11,11 @@ "test": "node Makefile.js test", "lint": "node Makefile.js lint", "fuzz": "node Makefile.js fuzz", - "release": "node Makefile.js release", - "ci-release": "node Makefile.js ciRelease", - "alpharelease": "node Makefile.js prerelease -- alpha", - "betarelease": "node Makefile.js prerelease -- beta", - "rcrelease": "node Makefile.js prerelease -- rc", + "generate-release": "node Makefile.js generateRelease", + "generate-alpharelease": "node Makefile.js generatePrerelease -- alpha", + "generate-betarelease": "node Makefile.js generatePrerelease -- beta", + "generate-rcrelease": "node Makefile.js generatePrerelease -- rc", + "publish-release": "node Makefile.js publishRelease", "docs": "node Makefile.js docs", "gensite": "node Makefile.js gensite", "browserify": "node Makefile.js browserify", @@ -39,7 +39,7 @@ "ajv": "^6.5.3", "chalk": "^2.1.0", "cross-spawn": "^6.0.5", - "debug": "^3.1.0", + "debug": "^4.0.1", "doctrine": "^2.1.0", "eslint-scope": "^4.0.0", "eslint-utils": "^1.3.1", @@ -66,12 +66,12 @@ "path-is-inside": "^1.0.2", "pluralize": "^7.0.0", "progress": "^2.0.0", - "regexpp": "^2.0.0", + "regexpp": "^2.0.1", "require-uncached": "^1.0.3", "semver": "^5.5.1", "strip-ansi": "^4.0.0", "strip-json-comments": "^2.0.1", - "table": "^4.0.3", + "table": "^5.0.2", "text-table": "^0.2.0" }, "devDependencies": { @@ -90,7 +90,7 @@ "eslint-plugin-eslint-plugin": "^1.2.0", "eslint-plugin-node": "^7.0.1", "eslint-plugin-rulesdir": "^0.1.0", - "eslint-release": "^0.11.1", + "eslint-release": "^1.0.0", "eslint-rule-composer": "^0.3.0", "eslump": "^1.6.2", "esprima": "^4.0.1", diff --git a/tests/bin/eslint.js b/tests/bin/eslint.js index 75e7c2c4b7c8..7749b12f1071 100644 --- a/tests/bin/eslint.js +++ b/tests/bin/eslint.js @@ -333,12 +333,28 @@ describe("bin/eslint.js", () => { return Promise.all([exitCodeAssertion, outputAssertion]); }); + it("prints the error message exactly once to stderr in the event of a crash", () => { + const child = runESLint(["--rule=no-restricted-syntax:[error, 'Invalid Selector [[[']", "Makefile.js"]); + const exitCodeAssertion = assertExitCode(child, 2); + const outputAssertion = getOutput(child).then(output => { + const expectedSubstring = "Syntax error in selector"; + + assert.strictEqual(output.stdout, ""); + assert.include(output.stderr, expectedSubstring); + + // The message should appear exactly once in stderr + assert.strictEqual(output.stderr.indexOf(expectedSubstring), output.stderr.lastIndexOf(expectedSubstring)); + }); + + return Promise.all([exitCodeAssertion, outputAssertion]); + }); + it("prints the error message pointing to line of code", () => { const invalidConfig = `${__dirname}/../fixtures/bin/.eslintrc.yml`; const child = runESLint(["--no-ignore", invalidConfig]); const exitCodeAssertion = assertExitCode(child, 2); const outputAssertion = getOutput(child).then(output => { - const expectedSubstring = "Error: bad indentation of a mapping entry at line"; + const expectedSubstring = ": bad indentation of a mapping entry at line"; assert.strictEqual(output.stdout, ""); assert.include(output.stderr, expectedSubstring); diff --git a/tests/fixtures/parsers/typescript-parsers/type-alias.js b/tests/fixtures/parsers/typescript-parsers/type-alias.js new file mode 100644 index 000000000000..f1d68c486942 --- /dev/null +++ b/tests/fixtures/parsers/typescript-parsers/type-alias.js @@ -0,0 +1,155 @@ +"use strict"; + +exports.parse = () => ({ + type: "Program", + range: [0, 16], + loc: { + start: { line: 1, column: 0 }, + end: { line: 1, column: 16 } + }, + body: [ + { + type: "VariableDeclaration", + range: [0, 16], + loc: { + start: { line: 1, column: 0 }, + end: { line: 1, column: 16 } + }, + kind: "type", + declarations: [ + { + type: "VariableDeclarator", + id: { + type: "Identifier", + range: [ + 5, + 8 + ], + loc: { + start: { line: 1, column: 5 }, + end: { line: 1, column: 8 } + }, + name: "Foo" + }, + init: { + type: "TSTypeReference", + range: [14, 15], + loc: { + start: { line: 1, column: 14 }, + end: { line: 1, column: 15 } + }, + typeName: { + type: "Identifier", + range: [14, 15], + loc: { + start: { line: 1, column: 14 }, + end: { line: 1, column: 15 } + }, + name: "T" + } + }, + range: [5, 16], + loc: { + start: { line: 1, column: 5 }, + end: { line: 1, column: 16 } + }, + typeParameters: { + type: "TSTypeParameterDeclaration", + range: [8, 11], + loc: { + start: { line: 1, column: 8 }, + end: { line: 1, column: 11 } + }, + params: [ + { + type: "TSTypeParameter", + range: [9, 10], + loc: { + start: { line: 1, column: 9 }, + end: { line: 1, column: 10 } + }, + name: "T" + } + ] + } + } + ] + } + ], + sourceType: "script", + tokens: [ + { + type: "Identifier", + value: "type", + range: [0, 4], + loc: { + start: { line: 1, column: 0 }, + end: { line: 1, column: 4 } + } + }, + { + type: "Identifier", + value: "Foo", + range: [5, 8], + loc: { + start: { line: 1, column: 5 }, + end: { line: 1, column: 8 } + } + }, + { + type: "Punctuator", + value: "<", + range: [8, 9], + loc: { + start: { line: 1, column: 8 }, + end: { line: 1, column: 9 } + } + }, + { + type: "Identifier", + value: "T", + range: [9, 10], + loc: { + start: { line: 1, column: 9 }, + end: { line: 1, column: 10 } + } + }, + { + type: "Punctuator", + value: ">", + range: [10, 11], + loc: { + start: { line: 1, column: 10 }, + end: { line: 1, column: 11 } + } + }, + { + type: "Punctuator", + value: "=", + range: [12, 13], + loc: { + start: { line: 1, column: 12 }, + end: { line: 1, column: 13 } + } + }, + { + type: "Identifier", + value: "T", + 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 } + } + } + ], + comments: [] +}) diff --git a/tests/lib/config/config-validator.js b/tests/lib/config/config-validator.js index 456b1518364e..b4cb2e41d8d4 100644 --- a/tests/lib/config/config-validator.js +++ b/tests/lib/config/config-validator.js @@ -342,7 +342,7 @@ describe("Validator", () => { it("should throw if override has an empty files array", () => { const fn = validator.validate.bind(null, { overrides: [{ files: [] }] }, "tests", ruleMapper, linter.environments); - assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Property \"overrides[0].files\" is the wrong type (expected string but got `[]`).\n\t- \"overrides[0].files\" should NOT have less than 1 items. Value: [].\n\t- \"overrides[0].files\" should match exactly one schema in oneOf. Value: [].\n"); + assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Property \"overrides[0].files\" is the wrong type (expected string but got `[]`).\n\t- \"overrides[0].files\" should NOT have fewer than 1 items. Value: [].\n\t- \"overrides[0].files\" should match exactly one schema in oneOf. Value: [].\n"); }); it("should throw if override has nested overrides", () => { diff --git a/tests/lib/linter.js b/tests/lib/linter.js index dd8e10b951e1..5b2f2c31a7c7 100644 --- a/tests/lib/linter.js +++ b/tests/lib/linter.js @@ -76,6 +76,14 @@ describe("Linter", () => { sandbox.verifyAndRestore(); }); + describe("Static Members", () => { + describe("version", () => { + it("should return same version as instance property", () => { + assert.strictEqual(Linter.version, linter.version); + }); + }); + }); + describe("when using events", () => { const code = TEST_CODE; diff --git a/tests/lib/rules/camelcase.js b/tests/lib/rules/camelcase.js index 481842f97392..3d2b7516c39d 100644 --- a/tests/lib/rules/camelcase.js +++ b/tests/lib/rules/camelcase.js @@ -181,6 +181,18 @@ ruleTester.run("camelcase", rule, { { code: "function foo({ trailing_ }) {}", parserOptions: { ecmaVersion: 6 } + }, + { + code: "ignored_foo = 0;", + options: [{ allow: ["ignored_foo"] }] + }, + { + code: "ignored_foo = 0; ignored_bar = 1;", + options: [{ allow: ["ignored_foo", "ignored_bar"] }] + }, + { + code: "user_id = 0;", + options: [{ allow: ["_id$"] }] } ], invalid: [ @@ -555,6 +567,26 @@ ruleTester.run("camelcase", rule, { type: "Identifier" } ] + }, + { + code: "not_ignored_foo = 0;", + options: [{ allow: ["ignored_bar"] }], + errors: [ + { + message: "Identifier 'not_ignored_foo' is not in camel case.", + type: "Identifier" + } + ] + }, + { + code: "not_ignored_foo = 0;", + options: [{ allow: ["_id$"] }], + errors: [ + { + message: "Identifier 'not_ignored_foo' is not in camel case.", + type: "Identifier" + } + ] } ] }); diff --git a/tests/lib/rules/no-extra-bind.js b/tests/lib/rules/no-extra-bind.js index 4dfec87ac6c9..4e5a08f36608 100644 --- a/tests/lib/rules/no-extra-bind.js +++ b/tests/lib/rules/no-extra-bind.js @@ -70,10 +70,32 @@ ruleTester.run("no-extra-bind", rule, { output: "var a = function() { function c(){ this.d } }", errors }, + { + code: "var a = function() { return 1; }.bind(this)", + output: "var a = function() { return 1; }", + errors + }, { code: "var a = function() { (function(){ (function(){ this.d }.bind(c)) }) }.bind(b)", output: "var a = function() { (function(){ (function(){ this.d }.bind(c)) }) }", errors: [{ messageId: "unexpected", type: "CallExpression", column: 71 }] + }, + + // Should not autofix if bind expression args have side effects + { + code: "var a = function() {}.bind(b++)", + output: null, + errors + }, + { + code: "var a = function() {}.bind(b())", + output: null, + errors + }, + { + code: "var a = function() {}.bind(b.c)", + output: null, + errors } ] }); diff --git a/tests/lib/rules/no-invalid-regexp.js b/tests/lib/rules/no-invalid-regexp.js index 305f4e0d3832..0062a5bbc5a7 100644 --- a/tests/lib/rules/no-invalid-regexp.js +++ b/tests/lib/rules/no-invalid-regexp.js @@ -46,6 +46,12 @@ ruleTester.run("no-invalid-regexp", rule, { invalid: [ { code: "RegExp('[');", errors: [{ message: "Invalid regular expression: /[/: Unterminated character class.", type: "CallExpression" }] }, { code: "RegExp('.', 'z');", errors: [{ message: "Invalid flags supplied to RegExp constructor 'z'.", type: "CallExpression" }] }, - { code: "new RegExp(')');", errors: [{ message: "Invalid regular expression: /)/: Unmatched ')'.", type: "NewExpression" }] } + { code: "new RegExp(')');", errors: [{ message: "Invalid regular expression: /)/: Unmatched ')'.", type: "NewExpression" }] }, + + // https://github.com/eslint/eslint/issues/10861 + { + code: String.raw`new RegExp('\\');`, + errors: [{ message: "Invalid regular expression: /\\/: \\ at end of pattern.", type: "NewExpression" }] + } ] }); diff --git a/tests/lib/rules/no-tabs.js b/tests/lib/rules/no-tabs.js index a01d9dc16619..b4989037d308 100644 --- a/tests/lib/rules/no-tabs.js +++ b/tests/lib/rules/no-tabs.js @@ -23,7 +23,16 @@ ruleTester.run("no-tabs", rule, { "function test(){\n}", "function test(){\n" + " // sdfdsf \n" + - "}" + "}", + + { + code: "\tdoSomething();", + options: [{ allowIndentationTabs: true }] + }, + { + code: "\t// comment", + options: [{ allowIndentationTabs: true }] + } ], invalid: [ { @@ -31,7 +40,7 @@ ruleTester.run("no-tabs", rule, { errors: [{ message: ERROR_MESSAGE, line: 1, - column: 18 + column: 17 }] }, { @@ -39,7 +48,7 @@ ruleTester.run("no-tabs", rule, { errors: [{ message: ERROR_MESSAGE, line: 1, - column: 6 + column: 5 }] }, { @@ -50,7 +59,7 @@ ruleTester.run("no-tabs", rule, { errors: [{ message: ERROR_MESSAGE, line: 2, - column: 6 + column: 5 }] }, { @@ -61,7 +70,7 @@ ruleTester.run("no-tabs", rule, { errors: [{ message: ERROR_MESSAGE, line: 1, - column: 10 + column: 9 }] }, { @@ -73,14 +82,23 @@ ruleTester.run("no-tabs", rule, { { message: ERROR_MESSAGE, line: 2, - column: 6 + column: 5 }, { message: ERROR_MESSAGE, line: 3, - column: 2 + column: 1 } ] + }, + { + code: "\t// Comment with leading tab \t and inline tab", + options: [{ allowIndentationTabs: true }], + errors: [{ + message: ERROR_MESSAGE, + line: 1, + column: 30 + }] } ] }); diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js index f188f8ce30ae..316df18e09aa 100644 --- a/tests/lib/rules/no-unused-vars.js +++ b/tests/lib/rules/no-unused-vars.js @@ -271,7 +271,11 @@ ruleTester.run("no-unused-vars", rule, { code: "(({a, ...rest}) => rest)", options: [{ args: "all", ignoreRestSiblings: true }], parserOptions: { ecmaVersion: 2018 } - } + }, + + // https://github.com/eslint/eslint/issues/10952 + "/*eslint use-every-a:1*/ !function(b, a) { return 1 }" + ], invalid: [ { code: "function foox() { return foox(); }", errors: [definedError("foox")] }, diff --git a/tests/lib/rules/one-var.js b/tests/lib/rules/one-var.js index 1f6626ad87e1..e5470d2e9ca3 100644 --- a/tests/lib/rules/one-var.js +++ b/tests/lib/rules/one-var.js @@ -263,6 +263,13 @@ ruleTester.run("one-var", rule, { options: ["consecutive"], parserOptions: { ecmaVersion: 6 } }, + + // https://github.com/eslint/eslint/issues/10784 + { + code: "const foo = require('foo'); const bar = 'bar';", + options: [{ const: "consecutive", separateRequires: true }], + parserOptions: { ecmaVersion: 6 } + }, { code: "var a = 0, b = 1; var c, d;", options: [{ initialized: "consecutive", uninitialized: "always" }] @@ -942,6 +949,17 @@ ruleTester.run("one-var", rule, { column: 1 }] }, + { + code: "var a = 1, b = 2", + output: "var a = 1; var b = 2", + options: ["never"], + errors: [{ + message: "Split 'var' declarations into multiple statements.", + type: "VariableDeclaration", + line: 1, + column: 1 + }] + }, { code: "var foo = require('foo'), bar;", output: null, diff --git a/tests/lib/rules/padding-line-between-statements.js b/tests/lib/rules/padding-line-between-statements.js index 3f72f8ff2d2a..82147d389d41 100644 --- a/tests/lib/rules/padding-line-between-statements.js +++ b/tests/lib/rules/padding-line-between-statements.js @@ -852,6 +852,35 @@ ruleTester.run("padding-line-between-statements", rule, { ] }, + //---------------------------------------------------------------------- + // iife + //---------------------------------------------------------------------- + + { + code: "(function(){\n})()\n\nvar a = 2;", + options: [ + { blankLine: "always", prev: "iife", next: "*" } + ] + }, + { + code: "+(function(){\n})()\n\nvar a = 2;", + options: [ + { blankLine: "always", prev: "iife", next: "*" } + ] + }, + { + code: "(function(){\n})()\nvar a = 2;", + options: [ + { blankLine: "never", prev: "iife", next: "*" } + ] + }, + { + code: "+(function(){\n})()\nvar a = 2;", + options: [ + { blankLine: "never", prev: "iife", next: "*" } + ] + }, + //---------------------------------------------------------------------- // import //---------------------------------------------------------------------- @@ -3371,6 +3400,43 @@ ruleTester.run("padding-line-between-statements", rule, { errors: [MESSAGE_ALWAYS] }, + //---------------------------------------------------------------------- + // iife + //---------------------------------------------------------------------- + + { + code: "(function(){\n})()\n\nvar a = 2;", + output: "(function(){\n})()\nvar a = 2;", + options: [ + { blankLine: "never", prev: "iife", next: "*" } + ], + errors: [MESSAGE_NEVER] + }, + { + code: "+(function(){\n})()\n\nvar a = 2;", + output: "+(function(){\n})()\nvar a = 2;", + options: [ + { blankLine: "never", prev: "iife", next: "*" } + ], + errors: [MESSAGE_NEVER] + }, + { + code: "(function(){\n})()\nvar a = 2;", + output: "(function(){\n})()\n\nvar a = 2;", + options: [ + { blankLine: "always", prev: "iife", next: "*" } + ], + errors: [MESSAGE_ALWAYS] + }, + { + code: "+(function(){\n})()\nvar a = 2;", + output: "+(function(){\n})()\n\nvar a = 2;", + options: [ + { blankLine: "always", prev: "iife", next: "*" } + ], + errors: [MESSAGE_ALWAYS] + }, + //---------------------------------------------------------------------- // import //---------------------------------------------------------------------- @@ -4247,6 +4313,14 @@ ruleTester.run("padding-line-between-statements", rule, { ], errors: [MESSAGE_NEVER] }, + { + code: "+(function(){\n})()\n\nvar a = 2;", + output: "+(function(){\n})()\nvar a = 2;", + options: [ + { blankLine: "never", prev: "block-like", next: "*" } + ], + errors: [MESSAGE_NEVER] + }, { code: "var a = function() {};\n\nvar b = 2;", output: "var a = function() {};\nvar b = 2;", diff --git a/tests/lib/rules/prefer-const.js b/tests/lib/rules/prefer-const.js index c7b8a93c2be6..f97320234616 100644 --- a/tests/lib/rules/prefer-const.js +++ b/tests/lib/rules/prefer-const.js @@ -117,6 +117,54 @@ ruleTester.run("prefer-const", rule, { parser: fixtureParser("babel-eslint5/destructuring-object-spread") }, + // https://github.com/eslint/eslint/issues/8308 + { + code: "let predicate; [typeNode.returnType, predicate] = foo();", + parserOptions: { ecmaVersion: 2018 } + }, + { + code: "let predicate; [typeNode.returnType, ...predicate] = foo();", + parserOptions: { ecmaVersion: 2018 } + }, + { + + // intentionally testing empty slot in destructuring assignment + code: "let predicate; [typeNode.returnType,, predicate] = foo();", + parserOptions: { ecmaVersion: 2018 } + }, + { + code: "let predicate; [typeNode.returnType=5, predicate] = foo();", + parserOptions: { ecmaVersion: 2018 } + }, + { + code: "let predicate; [[typeNode.returnType=5], predicate] = foo();", + parserOptions: { ecmaVersion: 2018 } + }, + { + code: "let predicate; [[typeNode.returnType, predicate]] = foo();", + parserOptions: { ecmaVersion: 2018 } + }, + { + code: "let predicate; [typeNode.returnType, [predicate]] = foo();", + parserOptions: { ecmaVersion: 2018 } + }, + { + code: "let predicate; [, [typeNode.returnType, predicate]] = foo();", + parserOptions: { ecmaVersion: 2018 } + }, + { + code: "let predicate; [, {foo:typeNode.returnType, predicate}] = foo();", + parserOptions: { ecmaVersion: 2018 } + }, + { + code: "let predicate; [, {foo:typeNode.returnType, ...predicate}] = foo();", + parserOptions: { ecmaVersion: 2018 } + }, + { + code: "let a; const b = {}; ({ a, c: b.c } = func());", + parserOptions: { ecmaVersion: 2018 } + }, + // ignoreReadBeforeAssign { code: "let x; function foo() { bar(x); } x = 0;", @@ -380,6 +428,33 @@ ruleTester.run("prefer-const", rule, { { message: "'y' is never reassigned. Use 'const' instead.", type: "Identifier" }, { message: "'z' is never reassigned. Use 'const' instead.", type: "Identifier" } ] + }, + + // https://github.com/eslint/eslint/issues/8308 + { + code: "let predicate; [, {foo:returnType, predicate}] = foo();", + output: null, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { message: "'predicate' is never reassigned. Use 'const' instead.", type: "Identifier" } + ] + }, + { + code: "let predicate; [, {foo:returnType, predicate}, ...bar ] = foo();", + output: null, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { message: "'predicate' is never reassigned. Use 'const' instead.", type: "Identifier" } + ] + }, + { + code: "let predicate; [, {foo:returnType, ...predicate} ] = foo();", + output: null, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { message: "'predicate' is never reassigned. Use 'const' instead.", type: "Identifier" } + ] } + ] }); diff --git a/tests/lib/rules/require-atomic-updates.js b/tests/lib/rules/require-atomic-updates.js index eabd521499cb..4128357773b7 100644 --- a/tests/lib/rules/require-atomic-updates.js +++ b/tests/lib/rules/require-atomic-updates.js @@ -51,7 +51,64 @@ ruleTester.run("require-atomic-updates", rule, { "let foo; function* x() { foo = bar + foo; }", "async function x() { let foo; bar(() => baz += 1); foo += await amount; }", "let foo; async function x() { foo = condition ? foo : await bar; }", - "async function x() { let foo; bar(() => { let foo; blah(foo); }); foo += await result; }" + "async function x() { let foo; bar(() => { let foo; blah(foo); }); foo += await result; }", + "let foo; async function x() { foo = foo + 1; await bar; }", + + + /* + * Ensure rule doesn't take exponential time in the number of branches + * (see https://github.com/eslint/eslint/issues/10893) + */ + ` + async function foo() { + if (1); + if (2); + if (3); + if (4); + if (5); + if (6); + if (7); + if (8); + if (9); + if (10); + if (11); + if (12); + if (13); + if (14); + if (15); + if (16); + if (17); + if (18); + if (19); + if (20); + } + `, + ` + async function foo() { + return [ + 1 ? a : b, + 2 ? a : b, + 3 ? a : b, + 4 ? a : b, + 5 ? a : b, + 6 ? a : b, + 7 ? a : b, + 8 ? a : b, + 9 ? a : b, + 10 ? a : b, + 11 ? a : b, + 12 ? a : b, + 13 ? a : b, + 14 ? a : b, + 15 ? a : b, + 16 ? a : b, + 17 ? a : b, + 18 ? a : b, + 19 ? a : b, + 20 ? a : b + ]; + } + ` ], invalid: [ @@ -59,6 +116,10 @@ ruleTester.run("require-atomic-updates", rule, { code: "let foo; async function x() { foo += await amount; }", errors: [{ messageId: "nonAtomicUpdate", data: { value: "foo" } }] }, + { + code: "if (1); let foo; async function x() { foo += await amount; }", + errors: [{ messageId: "nonAtomicUpdate", data: { value: "foo" } }] + }, { code: "let foo; async function x() { while (condition) { foo += await amount; } }", errors: [VARIABLE_ERROR] @@ -138,6 +199,14 @@ ruleTester.run("require-atomic-updates", rule, { { code: "async function x() { foo += await bar; }", errors: [VARIABLE_ERROR] + }, + { + code: "let foo = 0; async function x() { foo = (a ? b : foo) + await bar; if (baz); }", + errors: [VARIABLE_ERROR] + }, + { + code: "let foo = 0; async function x() { foo = (a ? b ? c ? d ? foo : e : f : g : h) + await bar; if (baz); }", + errors: [VARIABLE_ERROR] } ] }); diff --git a/tests/lib/rules/space-infix-ops.js b/tests/lib/rules/space-infix-ops.js index 3f98c88d4bb1..34eada46e426 100644 --- a/tests/lib/rules/space-infix-ops.js +++ b/tests/lib/rules/space-infix-ops.js @@ -18,8 +18,14 @@ const ruleTester = new RuleTester(); ruleTester.run("space-infix-ops", rule, { valid: [ "a + b", + "a + ++b", + "a++ + b", + "a++ + ++b", "a + b", "(a) + (b)", + "((a)) + ((b))", + "(((a))) + (((b)))", + "a + +b", "a + (b)", "a + +(b)", "a + (+(b))", @@ -38,7 +44,10 @@ ruleTester.run("space-infix-ops", rule, { { code: "function foo(a: number = 0) { }", parserOptions: { ecmaVersion: 6 }, parser: parser("type-annotations/function-parameter-type-annotation") }, { code: "function foo(): Bar { }", parserOptions: { ecmaVersion: 6 }, parser: parser("type-annotations/function-return-type-annotation") }, { code: "var foo: Bar = '';", parserOptions: { ecmaVersion: 6 }, parser: parser("type-annotations/variable-declaration-init-type-annotation") }, - { code: "const foo = function(a: number = 0): Bar { };", parserOptions: { ecmaVersion: 6 }, parser: parser("type-annotations/function-expression-type-annotation") } + { code: "const foo = function(a: number = 0): Bar { };", parserOptions: { ecmaVersion: 6 }, parser: parser("type-annotations/function-expression-type-annotation") }, + + // TypeScript Type Aliases + { code: "type Foo = T;", parserOptions: { ecmaVersion: 6 }, parser: parser("typescript-parsers/type-alias") } ], invalid: [ { diff --git a/tests/lib/testers/rule-tester.js b/tests/lib/testers/rule-tester.js index 4a860890605d..5a582f97678c 100644 --- a/tests/lib/testers/rule-tester.js +++ b/tests/lib/testers/rule-tester.js @@ -640,7 +640,7 @@ describe("RuleTester", () => { { code: "var answer = 6 * 7;", options: ["bar"], errors: [{ message: "Expected nothing." }] } ] }); - }, "Schema for rule no-invalid-schema is invalid:,\titems: should be object\n\titems[0].enum: should NOT have less than 1 items\n\titems: should match some schema in anyOf"); + }, "Schema for rule no-invalid-schema is invalid:,\titems: should be object\n\titems[0].enum: should NOT have fewer than 1 items\n\titems: should match some schema in anyOf"); });