Skip to content

Commit e41a1e0

Browse files
committedMay 3, 2019
✨ add node/prefer-promises rules (fixes #157, fixes #158)
1 parent 9143043 commit e41a1e0

File tree

9 files changed

+599
-0
lines changed

9 files changed

+599
-0
lines changed
 

‎.eslintrc.js

+11
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,16 @@ module.exports = {
5858
],
5959
},
6060
},
61+
{
62+
files: ["**/rules/prefer-promises/*.js"],
63+
rules: {
64+
"@mysticatea/eslint-plugin/require-meta-docs-url": [
65+
"error",
66+
{
67+
pattern: `https://github.com/mysticatea/eslint-plugin-node/blob/v${version}/docs/rules/prefer-promises/{{name}}.md`,
68+
},
69+
],
70+
},
71+
},
6172
],
6273
}

‎README.md

+2
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ $ npm install --save-dev eslint eslint-plugin-node
9292
| [node/prefer-global/text-encoder](./docs/rules/prefer-global/text-encoder.md) | enforce either `TextEncoder` or `require("util").TextEncoder` | |
9393
| [node/prefer-global/url-search-params](./docs/rules/prefer-global/url-search-params.md) | enforce either `URLSearchParams` or `require("url").URLSearchParams` | |
9494
| [node/prefer-global/url](./docs/rules/prefer-global/url.md) | enforce either `URL` or `require("url").URL` | |
95+
| [node/prefer-promises/dns](./docs/rules/prefer-promises/dns.md) | enforce `require("dns").promises` | |
96+
| [node/prefer-promises/fs](./docs/rules/prefer-promises/fs.md) | enforce `require("fs").promises` | |
9597

9698
### Deprecated rules
9799

‎docs/rules/prefer-promises/dns.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# enforce `require("dns").promises` (prefer-promises/dns)
2+
3+
Since Node.js v11.14.0, `require("dns").promises` API has been stable.
4+
Promise API and `async`/`await` syntax will make code more readable than callback API.
5+
6+
## Rule Details
7+
8+
This rule disallows callback API in favor of promise API.
9+
10+
Examples of :-1: **incorrect** code for this rule:
11+
12+
```js
13+
/*eslint node/prefer-promises/dns: [error]*/
14+
const dns = require("dns")
15+
16+
function lookup(hostname) {
17+
dns.lookup(hostname, (error, address, family) => {
18+
//...
19+
})
20+
}
21+
```
22+
23+
```js
24+
/*eslint node/prefer-promises/dns: [error]*/
25+
import dns from "dns"
26+
27+
function lookup(hostname) {
28+
dns.lookup(hostname, (error, address, family) => {
29+
//...
30+
})
31+
}
32+
```
33+
34+
Examples of :+1: **correct** code for this rule:
35+
36+
```js
37+
/*eslint node/prefer-promises/dns: [error]*/
38+
const { promises: dns } = require("dns")
39+
40+
async function lookup(hostname) {
41+
const { address, family } = await dns.lookup(hostname)
42+
//...
43+
}
44+
```
45+
46+
```js
47+
/*eslint node/prefer-promises/dns: [error]*/
48+
import { promises as dns } from "dns"
49+
50+
async function lookup(hostname) {
51+
const { address, family } = await dns.lookup(hostname)
52+
//...
53+
}
54+
```

‎docs/rules/prefer-promises/fs.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# enforce `require("fs").promises` (prefer-promises/fs)
2+
3+
Since Node.js v11.14.0, `require("fs").promises` API has been stable.
4+
Promise API and `async`/`await` syntax will make code more readable than callback API.
5+
6+
## Rule Details
7+
8+
This rule disallows callback API in favor of promise API.
9+
10+
Examples of :-1: **incorrect** code for this rule:
11+
12+
```js
13+
/*eslint node/prefer-promises/fs: [error]*/
14+
const fs = require("fs")
15+
16+
function readData(filePath) {
17+
fs.readFile(filePath, "utf8", (error, content) => {
18+
//...
19+
})
20+
}
21+
```
22+
23+
```js
24+
/*eslint node/prefer-promises/fs: [error]*/
25+
import fs from "fs"
26+
27+
function readData(filePath) {
28+
fs.readFile(filePath, "utf8", (error, content) => {
29+
//...
30+
})
31+
}
32+
```
33+
34+
Examples of :+1: **correct** code for this rule:
35+
36+
```js
37+
/*eslint node/prefer-promises/fs: [error]*/
38+
const { promises: fs } = require("fs")
39+
40+
async function readData(filePath) {
41+
const content = await fs.readFile(filePath, "utf8")
42+
//...
43+
}
44+
```
45+
46+
```js
47+
/*eslint node/prefer-promises/fs: [error]*/
48+
import { promises as fs } from "fs"
49+
50+
async function readData(filePath) {
51+
const content = await fs.readFile(filePath, "utf8")
52+
//...
53+
}
54+
```

‎lib/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ module.exports = {
3030
"prefer-global/text-encoder": require("./rules/prefer-global/text-encoder"),
3131
"prefer-global/url-search-params": require("./rules/prefer-global/url-search-params"),
3232
"prefer-global/url": require("./rules/prefer-global/url"),
33+
"prefer-promises/dns": require("./rules/prefer-promises/dns"),
34+
"prefer-promises/fs": require("./rules/prefer-promises/fs"),
3335
"process-exit-as-throw": require("./rules/process-exit-as-throw"),
3436
shebang: require("./rules/shebang"),
3537

‎lib/rules/prefer-promises/dns.js

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @author Toru Nagashima
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
"use strict"
6+
7+
const { CALL, CONSTRUCT, ReferenceTracker } = require("eslint-utils")
8+
9+
const trackMap = {
10+
dns: {
11+
lookup: { [CALL]: true },
12+
lookupService: { [CALL]: true },
13+
Resolver: { [CONSTRUCT]: true },
14+
getServers: { [CALL]: true },
15+
resolve: { [CALL]: true },
16+
resolve4: { [CALL]: true },
17+
resolve6: { [CALL]: true },
18+
resolveAny: { [CALL]: true },
19+
resolveCname: { [CALL]: true },
20+
resolveMx: { [CALL]: true },
21+
resolveNaptr: { [CALL]: true },
22+
resolveNs: { [CALL]: true },
23+
resolvePtr: { [CALL]: true },
24+
resolveSoa: { [CALL]: true },
25+
resolveSrv: { [CALL]: true },
26+
resolveTxt: { [CALL]: true },
27+
reverse: { [CALL]: true },
28+
setServers: { [CALL]: true },
29+
},
30+
}
31+
32+
module.exports = {
33+
meta: {
34+
docs: {
35+
description: 'enforce `require("dns").promises`',
36+
category: "Stylistic Issues",
37+
recommended: false,
38+
url:
39+
"https://github.com/mysticatea/eslint-plugin-node/blob/v8.0.1/docs/rules/prefer-promises/dns.md",
40+
},
41+
fixable: null,
42+
messages: {
43+
preferPromises: "Use 'dns.promises.{{name}}()' instead.",
44+
preferPromisesNew: "Use 'new dns.promises.{{name}}()' instead.",
45+
},
46+
schema: [],
47+
type: "suggestion",
48+
},
49+
50+
create(context) {
51+
return {
52+
"Program:exit"() {
53+
const scope = context.getScope()
54+
const tracker = new ReferenceTracker(scope, { mode: "legacy" })
55+
const references = [
56+
...tracker.iterateCjsReferences(trackMap),
57+
...tracker.iterateEsmReferences(trackMap),
58+
]
59+
60+
for (const { node, path } of references) {
61+
const name = path[path.length - 1]
62+
const isClass = name[0] === name[0].toUpperCase()
63+
context.report({
64+
node,
65+
messageId: isClass
66+
? "preferPromisesNew"
67+
: "preferPromises",
68+
data: { name },
69+
})
70+
}
71+
},
72+
}
73+
},
74+
}

‎lib/rules/prefer-promises/fs.js

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @author Toru Nagashima
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
"use strict"
6+
7+
const { CALL, ReferenceTracker } = require("eslint-utils")
8+
9+
const trackMap = {
10+
fs: {
11+
access: { [CALL]: true },
12+
copyFile: { [CALL]: true },
13+
open: { [CALL]: true },
14+
rename: { [CALL]: true },
15+
truncate: { [CALL]: true },
16+
rmdir: { [CALL]: true },
17+
mkdir: { [CALL]: true },
18+
readdir: { [CALL]: true },
19+
readlink: { [CALL]: true },
20+
symlink: { [CALL]: true },
21+
lstat: { [CALL]: true },
22+
stat: { [CALL]: true },
23+
link: { [CALL]: true },
24+
unlink: { [CALL]: true },
25+
chmod: { [CALL]: true },
26+
lchmod: { [CALL]: true },
27+
lchown: { [CALL]: true },
28+
chown: { [CALL]: true },
29+
utimes: { [CALL]: true },
30+
realpath: { [CALL]: true },
31+
mkdtemp: { [CALL]: true },
32+
writeFile: { [CALL]: true },
33+
appendFile: { [CALL]: true },
34+
readFile: { [CALL]: true },
35+
},
36+
}
37+
38+
module.exports = {
39+
meta: {
40+
docs: {
41+
description: 'enforce `require("fs").promises`',
42+
category: "Stylistic Issues",
43+
recommended: false,
44+
url:
45+
"https://github.com/mysticatea/eslint-plugin-node/blob/v8.0.1/docs/rules/prefer-promises/fs.md",
46+
},
47+
fixable: null,
48+
messages: {
49+
preferPromises: "Use 'fs.promises.{{name}}()' instead.",
50+
},
51+
schema: [],
52+
type: "suggestion",
53+
},
54+
55+
create(context) {
56+
return {
57+
"Program:exit"() {
58+
const scope = context.getScope()
59+
const tracker = new ReferenceTracker(scope, { mode: "legacy" })
60+
const references = [
61+
...tracker.iterateCjsReferences(trackMap),
62+
...tracker.iterateEsmReferences(trackMap),
63+
]
64+
65+
for (const { node, path } of references) {
66+
const name = path[path.length - 1]
67+
context.report({
68+
node,
69+
messageId: "preferPromises",
70+
data: { name },
71+
})
72+
}
73+
},
74+
}
75+
},
76+
}
+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* @author Toru Nagashima
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
"use strict"
6+
7+
const RuleTester = require("eslint").RuleTester
8+
const rule = require("../../../../lib/rules/prefer-promises/dns")
9+
10+
new RuleTester({
11+
parserOptions: {
12+
ecmaVersion: 2015,
13+
sourceType: "module",
14+
},
15+
globals: {
16+
require: false,
17+
},
18+
}).run("prefer-promises/dns", rule, {
19+
valid: [
20+
"const dns = require('dns'); dns.lookupSync()",
21+
"const dns = require('dns'); dns.promises.lookup()",
22+
"const {promises} = require('dns'); promises.lookup()",
23+
"const {promises: dns} = require('dns'); dns.lookup()",
24+
"const {promises: {lookup}} = require('dns'); lookup()",
25+
"import dns from 'dns'; dns.promises.lookup()",
26+
"import * as dns from 'dns'; dns.promises.lookup()",
27+
"import {promises} from 'dns'; promises.lookup()",
28+
"import {promises as dns} from 'dns'; dns.lookup()",
29+
],
30+
invalid: [
31+
{
32+
code: "const dns = require('dns'); dns.lookup()",
33+
errors: [{ messageId: "preferPromises", data: { name: "lookup" } }],
34+
},
35+
{
36+
code: "const {lookup} = require('dns'); lookup()",
37+
errors: [{ messageId: "preferPromises", data: { name: "lookup" } }],
38+
},
39+
{
40+
code: "import dns from 'dns'; dns.lookup()",
41+
errors: [{ messageId: "preferPromises", data: { name: "lookup" } }],
42+
},
43+
{
44+
code: "import * as dns from 'dns'; dns.lookup()",
45+
errors: [{ messageId: "preferPromises", data: { name: "lookup" } }],
46+
},
47+
{
48+
code: "import {lookup} from 'dns'; lookup()",
49+
errors: [{ messageId: "preferPromises", data: { name: "lookup" } }],
50+
},
51+
52+
// Other members
53+
{
54+
code: "const dns = require('dns'); dns.lookupService()",
55+
errors: [
56+
{
57+
messageId: "preferPromises",
58+
data: { name: "lookupService" },
59+
},
60+
],
61+
},
62+
{
63+
code: "const dns = require('dns'); new dns.Resolver()",
64+
errors: [
65+
{ messageId: "preferPromisesNew", data: { name: "Resolver" } },
66+
],
67+
},
68+
{
69+
code: "const dns = require('dns'); dns.getServers()",
70+
errors: [
71+
{ messageId: "preferPromises", data: { name: "getServers" } },
72+
],
73+
},
74+
{
75+
code: "const dns = require('dns'); dns.resolve()",
76+
errors: [
77+
{ messageId: "preferPromises", data: { name: "resolve" } },
78+
],
79+
},
80+
{
81+
code: "const dns = require('dns'); dns.resolve4()",
82+
errors: [
83+
{ messageId: "preferPromises", data: { name: "resolve4" } },
84+
],
85+
},
86+
{
87+
code: "const dns = require('dns'); dns.resolve6()",
88+
errors: [
89+
{ messageId: "preferPromises", data: { name: "resolve6" } },
90+
],
91+
},
92+
{
93+
code: "const dns = require('dns'); dns.resolveAny()",
94+
errors: [
95+
{ messageId: "preferPromises", data: { name: "resolveAny" } },
96+
],
97+
},
98+
{
99+
code: "const dns = require('dns'); dns.resolveCname()",
100+
errors: [
101+
{ messageId: "preferPromises", data: { name: "resolveCname" } },
102+
],
103+
},
104+
{
105+
code: "const dns = require('dns'); dns.resolveMx()",
106+
errors: [
107+
{ messageId: "preferPromises", data: { name: "resolveMx" } },
108+
],
109+
},
110+
{
111+
code: "const dns = require('dns'); dns.resolveNaptr()",
112+
errors: [
113+
{ messageId: "preferPromises", data: { name: "resolveNaptr" } },
114+
],
115+
},
116+
{
117+
code: "const dns = require('dns'); dns.resolveNs()",
118+
errors: [
119+
{ messageId: "preferPromises", data: { name: "resolveNs" } },
120+
],
121+
},
122+
{
123+
code: "const dns = require('dns'); dns.resolvePtr()",
124+
errors: [
125+
{ messageId: "preferPromises", data: { name: "resolvePtr" } },
126+
],
127+
},
128+
{
129+
code: "const dns = require('dns'); dns.resolveSoa()",
130+
errors: [
131+
{ messageId: "preferPromises", data: { name: "resolveSoa" } },
132+
],
133+
},
134+
{
135+
code: "const dns = require('dns'); dns.resolveSrv()",
136+
errors: [
137+
{ messageId: "preferPromises", data: { name: "resolveSrv" } },
138+
],
139+
},
140+
{
141+
code: "const dns = require('dns'); dns.resolveTxt()",
142+
errors: [
143+
{ messageId: "preferPromises", data: { name: "resolveTxt" } },
144+
],
145+
},
146+
{
147+
code: "const dns = require('dns'); dns.reverse()",
148+
errors: [
149+
{ messageId: "preferPromises", data: { name: "reverse" } },
150+
],
151+
},
152+
{
153+
code: "const dns = require('dns'); dns.setServers()",
154+
errors: [
155+
{ messageId: "preferPromises", data: { name: "setServers" } },
156+
],
157+
},
158+
],
159+
})

‎tests/lib/rules/prefer-promises/fs.js

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* @author Toru Nagashima
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
"use strict"
6+
7+
const RuleTester = require("eslint").RuleTester
8+
const rule = require("../../../../lib/rules/prefer-promises/fs")
9+
10+
new RuleTester({
11+
parserOptions: {
12+
ecmaVersion: 2015,
13+
sourceType: "module",
14+
},
15+
globals: {
16+
require: false,
17+
},
18+
}).run("prefer-promises/fs", rule, {
19+
valid: [
20+
"const fs = require('fs'); fs.createReadStream()",
21+
"const fs = require('fs'); fs.accessSync()",
22+
"const fs = require('fs'); fs.promises.access()",
23+
"const {promises} = require('fs'); promises.access()",
24+
"const {promises: fs} = require('fs'); fs.access()",
25+
"const {promises: {access}} = require('fs'); access()",
26+
"import fs from 'fs'; fs.promises.access()",
27+
"import * as fs from 'fs'; fs.promises.access()",
28+
"import {promises} from 'fs'; promises.access()",
29+
"import {promises as fs} from 'fs'; fs.access()",
30+
],
31+
invalid: [
32+
{
33+
code: "const fs = require('fs'); fs.access()",
34+
errors: [{ messageId: "preferPromises", data: { name: "access" } }],
35+
},
36+
{
37+
code: "const {access} = require('fs'); access()",
38+
errors: [{ messageId: "preferPromises", data: { name: "access" } }],
39+
},
40+
{
41+
code: "import fs from 'fs'; fs.access()",
42+
errors: [{ messageId: "preferPromises", data: { name: "access" } }],
43+
},
44+
{
45+
code: "import * as fs from 'fs'; fs.access()",
46+
errors: [{ messageId: "preferPromises", data: { name: "access" } }],
47+
},
48+
{
49+
code: "import {access} from 'fs'; access()",
50+
errors: [{ messageId: "preferPromises", data: { name: "access" } }],
51+
},
52+
53+
// Other members
54+
{
55+
code: "const fs = require('fs'); fs.copyFile()",
56+
errors: [
57+
{ messageId: "preferPromises", data: { name: "copyFile" } },
58+
],
59+
},
60+
{
61+
code: "const fs = require('fs'); fs.open()",
62+
errors: [{ messageId: "preferPromises", data: { name: "open" } }],
63+
},
64+
{
65+
code: "const fs = require('fs'); fs.rename()",
66+
errors: [{ messageId: "preferPromises", data: { name: "rename" } }],
67+
},
68+
{
69+
code: "const fs = require('fs'); fs.truncate()",
70+
errors: [
71+
{ messageId: "preferPromises", data: { name: "truncate" } },
72+
],
73+
},
74+
{
75+
code: "const fs = require('fs'); fs.rmdir()",
76+
errors: [{ messageId: "preferPromises", data: { name: "rmdir" } }],
77+
},
78+
{
79+
code: "const fs = require('fs'); fs.mkdir()",
80+
errors: [{ messageId: "preferPromises", data: { name: "mkdir" } }],
81+
},
82+
{
83+
code: "const fs = require('fs'); fs.readdir()",
84+
errors: [
85+
{ messageId: "preferPromises", data: { name: "readdir" } },
86+
],
87+
},
88+
{
89+
code: "const fs = require('fs');fs.readlink()",
90+
errors: [
91+
{ messageId: "preferPromises", data: { name: "readlink" } },
92+
],
93+
},
94+
{
95+
code: "const fs = require('fs'); fs.symlink()",
96+
errors: [
97+
{ messageId: "preferPromises", data: { name: "symlink" } },
98+
],
99+
},
100+
{
101+
code: "const fs = require('fs'); fs.lstat()",
102+
errors: [{ messageId: "preferPromises", data: { name: "lstat" } }],
103+
},
104+
{
105+
code: "const fs = require('fs'); fs.stat()",
106+
errors: [{ messageId: "preferPromises", data: { name: "stat" } }],
107+
},
108+
{
109+
code: "const fs = require('fs'); fs.link()",
110+
errors: [{ messageId: "preferPromises", data: { name: "link" } }],
111+
},
112+
{
113+
code: "const fs = require('fs'); fs.unlink()",
114+
errors: [{ messageId: "preferPromises", data: { name: "unlink" } }],
115+
},
116+
{
117+
code: "const fs = require('fs'); fs.chmod()",
118+
errors: [{ messageId: "preferPromises", data: { name: "chmod" } }],
119+
},
120+
{
121+
code: "const fs = require('fs'); fs.lchmod()",
122+
errors: [{ messageId: "preferPromises", data: { name: "lchmod" } }],
123+
},
124+
{
125+
code: "const fs = require('fs'); fs.lchown()",
126+
errors: [{ messageId: "preferPromises", data: { name: "lchown" } }],
127+
},
128+
{
129+
code: "const fs = require('fs'); fs.chown()",
130+
errors: [{ messageId: "preferPromises", data: { name: "chown" } }],
131+
},
132+
{
133+
code: "const fs = require('fs'); fs.utimes()",
134+
errors: [{ messageId: "preferPromises", data: { name: "utimes" } }],
135+
},
136+
{
137+
code: "const fs = require('fs'); fs.realpath()",
138+
errors: [
139+
{ messageId: "preferPromises", data: { name: "realpath" } },
140+
],
141+
},
142+
{
143+
code: "const fs = require('fs'); fs.mkdtemp()",
144+
errors: [
145+
{ messageId: "preferPromises", data: { name: "mkdtemp" } },
146+
],
147+
},
148+
{
149+
code: "const fs = require('fs'); fs.writeFile()",
150+
errors: [
151+
{ messageId: "preferPromises", data: { name: "writeFile" } },
152+
],
153+
},
154+
{
155+
code: "const fs = require('fs'); fs.appendFile()",
156+
errors: [
157+
{ messageId: "preferPromises", data: { name: "appendFile" } },
158+
],
159+
},
160+
{
161+
code: "const fs = require('fs'); fs.readFile()",
162+
errors: [
163+
{ messageId: "preferPromises", data: { name: "readFile" } },
164+
],
165+
},
166+
],
167+
})

0 commit comments

Comments
 (0)
Please sign in to comment.