From e41a1e08729430edca550636437738529721d286 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 3 May 2019 14:09:07 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20add=20node/prefer-promises=20rules?= =?UTF-8?q?=20(fixes=20#157,=20fixes=20#158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 11 ++ README.md | 2 + docs/rules/prefer-promises/dns.md | 54 ++++++++ docs/rules/prefer-promises/fs.md | 54 ++++++++ lib/index.js | 2 + lib/rules/prefer-promises/dns.js | 74 +++++++++++ lib/rules/prefer-promises/fs.js | 76 +++++++++++ tests/lib/rules/prefer-promises/dns.js | 159 +++++++++++++++++++++++ tests/lib/rules/prefer-promises/fs.js | 167 +++++++++++++++++++++++++ 9 files changed, 599 insertions(+) create mode 100644 docs/rules/prefer-promises/dns.md create mode 100644 docs/rules/prefer-promises/fs.md create mode 100644 lib/rules/prefer-promises/dns.js create mode 100644 lib/rules/prefer-promises/fs.js create mode 100644 tests/lib/rules/prefer-promises/dns.js create mode 100644 tests/lib/rules/prefer-promises/fs.js diff --git a/.eslintrc.js b/.eslintrc.js index 06916d92..d528f8f4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -58,5 +58,16 @@ module.exports = { ], }, }, + { + files: ["**/rules/prefer-promises/*.js"], + rules: { + "@mysticatea/eslint-plugin/require-meta-docs-url": [ + "error", + { + pattern: `https://github.com/mysticatea/eslint-plugin-node/blob/v${version}/docs/rules/prefer-promises/{{name}}.md`, + }, + ], + }, + }, ], } diff --git a/README.md b/README.md index 53e0f379..0a784ca6 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ $ npm install --save-dev eslint eslint-plugin-node | [node/prefer-global/text-encoder](./docs/rules/prefer-global/text-encoder.md) | enforce either `TextEncoder` or `require("util").TextEncoder` | | | [node/prefer-global/url-search-params](./docs/rules/prefer-global/url-search-params.md) | enforce either `URLSearchParams` or `require("url").URLSearchParams` | | | [node/prefer-global/url](./docs/rules/prefer-global/url.md) | enforce either `URL` or `require("url").URL` | | +| [node/prefer-promises/dns](./docs/rules/prefer-promises/dns.md) | enforce `require("dns").promises` | | +| [node/prefer-promises/fs](./docs/rules/prefer-promises/fs.md) | enforce `require("fs").promises` | | ### Deprecated rules diff --git a/docs/rules/prefer-promises/dns.md b/docs/rules/prefer-promises/dns.md new file mode 100644 index 00000000..0d1a7a68 --- /dev/null +++ b/docs/rules/prefer-promises/dns.md @@ -0,0 +1,54 @@ +# enforce `require("dns").promises` (prefer-promises/dns) + +Since Node.js v11.14.0, `require("dns").promises` API has been stable. +Promise API and `async`/`await` syntax will make code more readable than callback API. + +## Rule Details + +This rule disallows callback API in favor of promise API. + +Examples of :-1: **incorrect** code for this rule: + +```js +/*eslint node/prefer-promises/dns: [error]*/ +const dns = require("dns") + +function lookup(hostname) { + dns.lookup(hostname, (error, address, family) => { + //... + }) +} +``` + +```js +/*eslint node/prefer-promises/dns: [error]*/ +import dns from "dns" + +function lookup(hostname) { + dns.lookup(hostname, (error, address, family) => { + //... + }) +} +``` + +Examples of :+1: **correct** code for this rule: + +```js +/*eslint node/prefer-promises/dns: [error]*/ +const { promises: dns } = require("dns") + +async function lookup(hostname) { + const { address, family } = await dns.lookup(hostname) + //... +} +``` + +```js +/*eslint node/prefer-promises/dns: [error]*/ +import { promises as dns } from "dns" + +async function lookup(hostname) { + const { address, family } = await dns.lookup(hostname) + //... +} +``` diff --git a/docs/rules/prefer-promises/fs.md b/docs/rules/prefer-promises/fs.md new file mode 100644 index 00000000..24067271 --- /dev/null +++ b/docs/rules/prefer-promises/fs.md @@ -0,0 +1,54 @@ +# enforce `require("fs").promises` (prefer-promises/fs) + +Since Node.js v11.14.0, `require("fs").promises` API has been stable. +Promise API and `async`/`await` syntax will make code more readable than callback API. + +## Rule Details + +This rule disallows callback API in favor of promise API. + +Examples of :-1: **incorrect** code for this rule: + +```js +/*eslint node/prefer-promises/fs: [error]*/ +const fs = require("fs") + +function readData(filePath) { + fs.readFile(filePath, "utf8", (error, content) => { + //... + }) +} +``` + +```js +/*eslint node/prefer-promises/fs: [error]*/ +import fs from "fs" + +function readData(filePath) { + fs.readFile(filePath, "utf8", (error, content) => { + //... + }) +} +``` + +Examples of :+1: **correct** code for this rule: + +```js +/*eslint node/prefer-promises/fs: [error]*/ +const { promises: fs } = require("fs") + +async function readData(filePath) { + const content = await fs.readFile(filePath, "utf8") + //... +} +``` + +```js +/*eslint node/prefer-promises/fs: [error]*/ +import { promises as fs } from "fs" + +async function readData(filePath) { + const content = await fs.readFile(filePath, "utf8") + //... +} +``` diff --git a/lib/index.js b/lib/index.js index cb057f45..a45d5a2f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -30,6 +30,8 @@ module.exports = { "prefer-global/text-encoder": require("./rules/prefer-global/text-encoder"), "prefer-global/url-search-params": require("./rules/prefer-global/url-search-params"), "prefer-global/url": require("./rules/prefer-global/url"), + "prefer-promises/dns": require("./rules/prefer-promises/dns"), + "prefer-promises/fs": require("./rules/prefer-promises/fs"), "process-exit-as-throw": require("./rules/process-exit-as-throw"), shebang: require("./rules/shebang"), diff --git a/lib/rules/prefer-promises/dns.js b/lib/rules/prefer-promises/dns.js new file mode 100644 index 00000000..5f472f4c --- /dev/null +++ b/lib/rules/prefer-promises/dns.js @@ -0,0 +1,74 @@ +/** + * @author Toru Nagashima + * See LICENSE file in root directory for full license. + */ +"use strict" + +const { CALL, CONSTRUCT, ReferenceTracker } = require("eslint-utils") + +const trackMap = { + dns: { + lookup: { [CALL]: true }, + lookupService: { [CALL]: true }, + Resolver: { [CONSTRUCT]: true }, + getServers: { [CALL]: true }, + resolve: { [CALL]: true }, + resolve4: { [CALL]: true }, + resolve6: { [CALL]: true }, + resolveAny: { [CALL]: true }, + resolveCname: { [CALL]: true }, + resolveMx: { [CALL]: true }, + resolveNaptr: { [CALL]: true }, + resolveNs: { [CALL]: true }, + resolvePtr: { [CALL]: true }, + resolveSoa: { [CALL]: true }, + resolveSrv: { [CALL]: true }, + resolveTxt: { [CALL]: true }, + reverse: { [CALL]: true }, + setServers: { [CALL]: true }, + }, +} + +module.exports = { + meta: { + docs: { + description: 'enforce `require("dns").promises`', + category: "Stylistic Issues", + recommended: false, + url: + "https://github.com/mysticatea/eslint-plugin-node/blob/v8.0.1/docs/rules/prefer-promises/dns.md", + }, + fixable: null, + messages: { + preferPromises: "Use 'dns.promises.{{name}}()' instead.", + preferPromisesNew: "Use 'new dns.promises.{{name}}()' instead.", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + "Program:exit"() { + const scope = context.getScope() + const tracker = new ReferenceTracker(scope, { mode: "legacy" }) + const references = [ + ...tracker.iterateCjsReferences(trackMap), + ...tracker.iterateEsmReferences(trackMap), + ] + + for (const { node, path } of references) { + const name = path[path.length - 1] + const isClass = name[0] === name[0].toUpperCase() + context.report({ + node, + messageId: isClass + ? "preferPromisesNew" + : "preferPromises", + data: { name }, + }) + } + }, + } + }, +} diff --git a/lib/rules/prefer-promises/fs.js b/lib/rules/prefer-promises/fs.js new file mode 100644 index 00000000..d1df04d5 --- /dev/null +++ b/lib/rules/prefer-promises/fs.js @@ -0,0 +1,76 @@ +/** + * @author Toru Nagashima + * See LICENSE file in root directory for full license. + */ +"use strict" + +const { CALL, ReferenceTracker } = require("eslint-utils") + +const trackMap = { + fs: { + access: { [CALL]: true }, + copyFile: { [CALL]: true }, + open: { [CALL]: true }, + rename: { [CALL]: true }, + truncate: { [CALL]: true }, + rmdir: { [CALL]: true }, + mkdir: { [CALL]: true }, + readdir: { [CALL]: true }, + readlink: { [CALL]: true }, + symlink: { [CALL]: true }, + lstat: { [CALL]: true }, + stat: { [CALL]: true }, + link: { [CALL]: true }, + unlink: { [CALL]: true }, + chmod: { [CALL]: true }, + lchmod: { [CALL]: true }, + lchown: { [CALL]: true }, + chown: { [CALL]: true }, + utimes: { [CALL]: true }, + realpath: { [CALL]: true }, + mkdtemp: { [CALL]: true }, + writeFile: { [CALL]: true }, + appendFile: { [CALL]: true }, + readFile: { [CALL]: true }, + }, +} + +module.exports = { + meta: { + docs: { + description: 'enforce `require("fs").promises`', + category: "Stylistic Issues", + recommended: false, + url: + "https://github.com/mysticatea/eslint-plugin-node/blob/v8.0.1/docs/rules/prefer-promises/fs.md", + }, + fixable: null, + messages: { + preferPromises: "Use 'fs.promises.{{name}}()' instead.", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + "Program:exit"() { + const scope = context.getScope() + const tracker = new ReferenceTracker(scope, { mode: "legacy" }) + const references = [ + ...tracker.iterateCjsReferences(trackMap), + ...tracker.iterateEsmReferences(trackMap), + ] + + for (const { node, path } of references) { + const name = path[path.length - 1] + context.report({ + node, + messageId: "preferPromises", + data: { name }, + }) + } + }, + } + }, +} diff --git a/tests/lib/rules/prefer-promises/dns.js b/tests/lib/rules/prefer-promises/dns.js new file mode 100644 index 00000000..53de056f --- /dev/null +++ b/tests/lib/rules/prefer-promises/dns.js @@ -0,0 +1,159 @@ +/** + * @author Toru Nagashima + * See LICENSE file in root directory for full license. + */ +"use strict" + +const RuleTester = require("eslint").RuleTester +const rule = require("../../../../lib/rules/prefer-promises/dns") + +new RuleTester({ + parserOptions: { + ecmaVersion: 2015, + sourceType: "module", + }, + globals: { + require: false, + }, +}).run("prefer-promises/dns", rule, { + valid: [ + "const dns = require('dns'); dns.lookupSync()", + "const dns = require('dns'); dns.promises.lookup()", + "const {promises} = require('dns'); promises.lookup()", + "const {promises: dns} = require('dns'); dns.lookup()", + "const {promises: {lookup}} = require('dns'); lookup()", + "import dns from 'dns'; dns.promises.lookup()", + "import * as dns from 'dns'; dns.promises.lookup()", + "import {promises} from 'dns'; promises.lookup()", + "import {promises as dns} from 'dns'; dns.lookup()", + ], + invalid: [ + { + code: "const dns = require('dns'); dns.lookup()", + errors: [{ messageId: "preferPromises", data: { name: "lookup" } }], + }, + { + code: "const {lookup} = require('dns'); lookup()", + errors: [{ messageId: "preferPromises", data: { name: "lookup" } }], + }, + { + code: "import dns from 'dns'; dns.lookup()", + errors: [{ messageId: "preferPromises", data: { name: "lookup" } }], + }, + { + code: "import * as dns from 'dns'; dns.lookup()", + errors: [{ messageId: "preferPromises", data: { name: "lookup" } }], + }, + { + code: "import {lookup} from 'dns'; lookup()", + errors: [{ messageId: "preferPromises", data: { name: "lookup" } }], + }, + + // Other members + { + code: "const dns = require('dns'); dns.lookupService()", + errors: [ + { + messageId: "preferPromises", + data: { name: "lookupService" }, + }, + ], + }, + { + code: "const dns = require('dns'); new dns.Resolver()", + errors: [ + { messageId: "preferPromisesNew", data: { name: "Resolver" } }, + ], + }, + { + code: "const dns = require('dns'); dns.getServers()", + errors: [ + { messageId: "preferPromises", data: { name: "getServers" } }, + ], + }, + { + code: "const dns = require('dns'); dns.resolve()", + errors: [ + { messageId: "preferPromises", data: { name: "resolve" } }, + ], + }, + { + code: "const dns = require('dns'); dns.resolve4()", + errors: [ + { messageId: "preferPromises", data: { name: "resolve4" } }, + ], + }, + { + code: "const dns = require('dns'); dns.resolve6()", + errors: [ + { messageId: "preferPromises", data: { name: "resolve6" } }, + ], + }, + { + code: "const dns = require('dns'); dns.resolveAny()", + errors: [ + { messageId: "preferPromises", data: { name: "resolveAny" } }, + ], + }, + { + code: "const dns = require('dns'); dns.resolveCname()", + errors: [ + { messageId: "preferPromises", data: { name: "resolveCname" } }, + ], + }, + { + code: "const dns = require('dns'); dns.resolveMx()", + errors: [ + { messageId: "preferPromises", data: { name: "resolveMx" } }, + ], + }, + { + code: "const dns = require('dns'); dns.resolveNaptr()", + errors: [ + { messageId: "preferPromises", data: { name: "resolveNaptr" } }, + ], + }, + { + code: "const dns = require('dns'); dns.resolveNs()", + errors: [ + { messageId: "preferPromises", data: { name: "resolveNs" } }, + ], + }, + { + code: "const dns = require('dns'); dns.resolvePtr()", + errors: [ + { messageId: "preferPromises", data: { name: "resolvePtr" } }, + ], + }, + { + code: "const dns = require('dns'); dns.resolveSoa()", + errors: [ + { messageId: "preferPromises", data: { name: "resolveSoa" } }, + ], + }, + { + code: "const dns = require('dns'); dns.resolveSrv()", + errors: [ + { messageId: "preferPromises", data: { name: "resolveSrv" } }, + ], + }, + { + code: "const dns = require('dns'); dns.resolveTxt()", + errors: [ + { messageId: "preferPromises", data: { name: "resolveTxt" } }, + ], + }, + { + code: "const dns = require('dns'); dns.reverse()", + errors: [ + { messageId: "preferPromises", data: { name: "reverse" } }, + ], + }, + { + code: "const dns = require('dns'); dns.setServers()", + errors: [ + { messageId: "preferPromises", data: { name: "setServers" } }, + ], + }, + ], +}) diff --git a/tests/lib/rules/prefer-promises/fs.js b/tests/lib/rules/prefer-promises/fs.js new file mode 100644 index 00000000..b9c66a92 --- /dev/null +++ b/tests/lib/rules/prefer-promises/fs.js @@ -0,0 +1,167 @@ +/** + * @author Toru Nagashima + * See LICENSE file in root directory for full license. + */ +"use strict" + +const RuleTester = require("eslint").RuleTester +const rule = require("../../../../lib/rules/prefer-promises/fs") + +new RuleTester({ + parserOptions: { + ecmaVersion: 2015, + sourceType: "module", + }, + globals: { + require: false, + }, +}).run("prefer-promises/fs", rule, { + valid: [ + "const fs = require('fs'); fs.createReadStream()", + "const fs = require('fs'); fs.accessSync()", + "const fs = require('fs'); fs.promises.access()", + "const {promises} = require('fs'); promises.access()", + "const {promises: fs} = require('fs'); fs.access()", + "const {promises: {access}} = require('fs'); access()", + "import fs from 'fs'; fs.promises.access()", + "import * as fs from 'fs'; fs.promises.access()", + "import {promises} from 'fs'; promises.access()", + "import {promises as fs} from 'fs'; fs.access()", + ], + invalid: [ + { + code: "const fs = require('fs'); fs.access()", + errors: [{ messageId: "preferPromises", data: { name: "access" } }], + }, + { + code: "const {access} = require('fs'); access()", + errors: [{ messageId: "preferPromises", data: { name: "access" } }], + }, + { + code: "import fs from 'fs'; fs.access()", + errors: [{ messageId: "preferPromises", data: { name: "access" } }], + }, + { + code: "import * as fs from 'fs'; fs.access()", + errors: [{ messageId: "preferPromises", data: { name: "access" } }], + }, + { + code: "import {access} from 'fs'; access()", + errors: [{ messageId: "preferPromises", data: { name: "access" } }], + }, + + // Other members + { + code: "const fs = require('fs'); fs.copyFile()", + errors: [ + { messageId: "preferPromises", data: { name: "copyFile" } }, + ], + }, + { + code: "const fs = require('fs'); fs.open()", + errors: [{ messageId: "preferPromises", data: { name: "open" } }], + }, + { + code: "const fs = require('fs'); fs.rename()", + errors: [{ messageId: "preferPromises", data: { name: "rename" } }], + }, + { + code: "const fs = require('fs'); fs.truncate()", + errors: [ + { messageId: "preferPromises", data: { name: "truncate" } }, + ], + }, + { + code: "const fs = require('fs'); fs.rmdir()", + errors: [{ messageId: "preferPromises", data: { name: "rmdir" } }], + }, + { + code: "const fs = require('fs'); fs.mkdir()", + errors: [{ messageId: "preferPromises", data: { name: "mkdir" } }], + }, + { + code: "const fs = require('fs'); fs.readdir()", + errors: [ + { messageId: "preferPromises", data: { name: "readdir" } }, + ], + }, + { + code: "const fs = require('fs');fs.readlink()", + errors: [ + { messageId: "preferPromises", data: { name: "readlink" } }, + ], + }, + { + code: "const fs = require('fs'); fs.symlink()", + errors: [ + { messageId: "preferPromises", data: { name: "symlink" } }, + ], + }, + { + code: "const fs = require('fs'); fs.lstat()", + errors: [{ messageId: "preferPromises", data: { name: "lstat" } }], + }, + { + code: "const fs = require('fs'); fs.stat()", + errors: [{ messageId: "preferPromises", data: { name: "stat" } }], + }, + { + code: "const fs = require('fs'); fs.link()", + errors: [{ messageId: "preferPromises", data: { name: "link" } }], + }, + { + code: "const fs = require('fs'); fs.unlink()", + errors: [{ messageId: "preferPromises", data: { name: "unlink" } }], + }, + { + code: "const fs = require('fs'); fs.chmod()", + errors: [{ messageId: "preferPromises", data: { name: "chmod" } }], + }, + { + code: "const fs = require('fs'); fs.lchmod()", + errors: [{ messageId: "preferPromises", data: { name: "lchmod" } }], + }, + { + code: "const fs = require('fs'); fs.lchown()", + errors: [{ messageId: "preferPromises", data: { name: "lchown" } }], + }, + { + code: "const fs = require('fs'); fs.chown()", + errors: [{ messageId: "preferPromises", data: { name: "chown" } }], + }, + { + code: "const fs = require('fs'); fs.utimes()", + errors: [{ messageId: "preferPromises", data: { name: "utimes" } }], + }, + { + code: "const fs = require('fs'); fs.realpath()", + errors: [ + { messageId: "preferPromises", data: { name: "realpath" } }, + ], + }, + { + code: "const fs = require('fs'); fs.mkdtemp()", + errors: [ + { messageId: "preferPromises", data: { name: "mkdtemp" } }, + ], + }, + { + code: "const fs = require('fs'); fs.writeFile()", + errors: [ + { messageId: "preferPromises", data: { name: "writeFile" } }, + ], + }, + { + code: "const fs = require('fs'); fs.appendFile()", + errors: [ + { messageId: "preferPromises", data: { name: "appendFile" } }, + ], + }, + { + code: "const fs = require('fs'); fs.readFile()", + errors: [ + { messageId: "preferPromises", data: { name: "readFile" } }, + ], + }, + ], +})