Skip to content

Commit 36c3fbf

Browse files
feat: add support for ignoring sync methods from certain locations
This reverts 0779e2f.
1 parent 067b9bf commit 36c3fbf

File tree

15 files changed

+535
-8
lines changed

15 files changed

+535
-8
lines changed

docs/rules/no-sync.md

+57
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ fs.readFileSync(somePath).toString();
6161
#### ignores
6262

6363
You can `ignore` specific function names using this option.
64+
Additionally, if you are using TypeScript you can optionally specify where the function is declared.
6465

6566
Examples of **incorrect** code for this rule with the `{ ignores: ['readFileSync'] }` option:
6667

@@ -78,6 +79,62 @@ Examples of **correct** code for this rule with the `{ ignores: ['readFileSync']
7879
fs.readFileSync(somePath);
7980
```
8081

82+
##### Advanced (TypeScript only)
83+
84+
You can provide a list of specifiers to ignore. Specifiers are typed as follows:
85+
86+
```ts
87+
type Specifier =
88+
| string
89+
| {
90+
from: "file";
91+
path?: string;
92+
name?: string[];
93+
}
94+
| {
95+
from: "package";
96+
package?: string;
97+
name?: string[];
98+
}
99+
| {
100+
from: "lib";
101+
name?: string[];
102+
}
103+
```
104+
105+
###### From a file
106+
107+
Examples of **correct** code for this rule with the ignore file specifier:
108+
109+
```js
110+
/*eslint n/no-sync: ["error", { ignores: [{ from: 'file', path: './foo.ts' }]}] */
111+
112+
import { fooSync } from "./foo"
113+
fooSync()
114+
```
115+
116+
###### From a package
117+
118+
Examples of **correct** code for this rule with the ignore package specifier:
119+
120+
```js
121+
/*eslint n/no-sync: ["error", { ignores: [{ from: 'package', package: 'effect' }]}] */
122+
123+
import { Effect } from "effect"
124+
const value = Effect.runSync(Effect.succeed(42))
125+
```
126+
127+
###### From the TypeScript library
128+
129+
Examples of **correct** code for this rule with the ignore lib specifier:
130+
131+
```js
132+
/*eslint n/no-sync: ["error", { ignores: [{ from: 'lib' }]}] */
133+
134+
const stylesheet = new CSSStyleSheet()
135+
stylesheet.replaceSync("body { font-size: 1.4em; } p { color: red; }")
136+
```
137+
81138
## 🔎 Implementation
82139

83140
- [Rule source](../../lib/rules/no-sync.js)

lib/rules/no-sync.js

+110-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
*/
55
"use strict"
66

7+
const typeMatchesSpecifier =
8+
/** @type {import('ts-declaration-location').default} */ (
9+
/** @type {unknown} */ (require("ts-declaration-location"))
10+
)
11+
const getTypeOfNode = require("../util/get-type-of-node")
12+
const getParserServices = require("../util/get-parser-services")
13+
const getFullTypeName = require("../util/get-full-type-name")
14+
715
const selectors = [
816
// fs.readFileSync()
917
// readFileSync.call(null, 'path')
@@ -32,7 +40,56 @@ module.exports = {
3240
},
3341
ignores: {
3442
type: "array",
35-
items: { type: "string" },
43+
items: {
44+
oneOf: [
45+
{ type: "string" },
46+
{
47+
type: "object",
48+
properties: {
49+
from: { const: "file" },
50+
path: {
51+
type: "string",
52+
},
53+
name: {
54+
type: "array",
55+
items: {
56+
type: "string",
57+
},
58+
},
59+
},
60+
additionalProperties: false,
61+
},
62+
{
63+
type: "object",
64+
properties: {
65+
from: { const: "lib" },
66+
name: {
67+
type: "array",
68+
items: {
69+
type: "string",
70+
},
71+
},
72+
},
73+
additionalProperties: false,
74+
},
75+
{
76+
type: "object",
77+
properties: {
78+
from: { const: "package" },
79+
package: {
80+
type: "string",
81+
},
82+
name: {
83+
type: "array",
84+
items: {
85+
type: "string",
86+
},
87+
},
88+
},
89+
additionalProperties: false,
90+
},
91+
],
92+
},
3693
default: [],
3794
},
3895
},
@@ -57,15 +114,64 @@ module.exports = {
57114
* @returns {void}
58115
*/
59116
[selector.join(",")](node) {
60-
if (ignores.includes(node.name)) {
61-
return
117+
const parserServices = getParserServices(context)
118+
119+
/**
120+
* @type {import('typescript').Type | undefined | null}
121+
*/
122+
let type = undefined
123+
124+
/**
125+
* @type {string | undefined | null}
126+
*/
127+
let fullName = undefined
128+
129+
for (const ignore of ignores) {
130+
if (typeof ignore === "string") {
131+
if (ignore === node.name) {
132+
return
133+
}
134+
135+
continue
136+
}
137+
138+
if (
139+
parserServices === null ||
140+
parserServices.program === null
141+
) {
142+
throw new Error(
143+
'TypeScript parser services not available. Rule "n/no-sync" is configured to use "ignores" option with a non-string value. This requires TypeScript parser services to be available.'
144+
)
145+
}
146+
147+
type =
148+
type === undefined
149+
? getTypeOfNode(node, parserServices)
150+
: type
151+
152+
fullName =
153+
fullName === undefined
154+
? getFullTypeName(type)
155+
: fullName
156+
157+
if (
158+
typeMatchesSpecifier(
159+
parserServices.program,
160+
ignore,
161+
type
162+
) &&
163+
(ignore.name === undefined ||
164+
ignore.name.includes(fullName ?? node.name))
165+
) {
166+
return
167+
}
62168
}
63169

64170
context.report({
65171
node: node.parent,
66172
messageId: "noSync",
67173
data: {
68-
propertyName: node.name,
174+
propertyName: fullName ?? node.name,
69175
},
70176
})
71177
},

lib/util/get-full-type-name.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"use strict"
2+
3+
const ts = (() => {
4+
try {
5+
// eslint-disable-next-line n/no-unpublished-require
6+
return require("typescript")
7+
} catch {
8+
return null
9+
}
10+
})()
11+
12+
/**
13+
* @param {import('typescript').Type | null} type
14+
* @returns {string | null}
15+
*/
16+
module.exports = function getFullTypeName(type) {
17+
if (ts === null || type === null) {
18+
return null
19+
}
20+
21+
/**
22+
* @type {string[]}
23+
*/
24+
let nameParts = []
25+
let currentSymbol = type.getSymbol()
26+
while (currentSymbol !== undefined) {
27+
if (
28+
currentSymbol.valueDeclaration?.kind === ts.SyntaxKind.SourceFile ||
29+
currentSymbol.valueDeclaration?.kind ===
30+
ts.SyntaxKind.ModuleDeclaration
31+
) {
32+
break
33+
}
34+
35+
nameParts.unshift(currentSymbol.getName())
36+
currentSymbol =
37+
/** @type {import('typescript').Symbol & {parent: import('typescript').Symbol | undefined}} */ (
38+
currentSymbol
39+
).parent
40+
}
41+
42+
if (nameParts.length === 0) {
43+
return null
44+
}
45+
46+
return nameParts.join(".")
47+
}

lib/util/get-parser-services.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use strict"
2+
3+
const {
4+
getParserServices: getParserServicesFromTsEslint,
5+
} = require("@typescript-eslint/utils/eslint-utils")
6+
7+
/**
8+
* Get the TypeScript parser services.
9+
* If TypeScript isn't present, returns `null`.
10+
*
11+
* @param {import('eslint').Rule.RuleContext} context - rule context
12+
* @returns {import('@typescript-eslint/parser').ParserServices | null}
13+
*/
14+
module.exports = function getParserServices(context) {
15+
// Not using tseslint parser?
16+
if (
17+
context.sourceCode.parserServices?.esTreeNodeToTSNodeMap == null ||
18+
context.sourceCode.parserServices.tsNodeToESTreeNodeMap == null
19+
) {
20+
return null
21+
}
22+
23+
return getParserServicesFromTsEslint(/** @type {any} */ (context), true)
24+
}

lib/util/get-type-of-node.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"use strict"
2+
3+
/**
4+
* Get the type of a node.
5+
* If TypeScript isn't present, returns `null`.
6+
*
7+
* @param {import('estree').Node} node - A node
8+
* @param {import('@typescript-eslint/parser').ParserServices} parserServices - A parserServices
9+
* @returns {import('typescript').Type | null}
10+
*/
11+
module.exports = function getTypeOfNode(node, parserServices) {
12+
const { esTreeNodeToTSNodeMap, program } = parserServices
13+
if (program === null) {
14+
return null
15+
}
16+
const tsNode = esTreeNodeToTSNodeMap.get(/** @type {any} */ (node))
17+
const checker = program.getTypeChecker()
18+
const nodeType = checker.getTypeAtLocation(tsNode)
19+
const constrained = checker.getBaseConstraintOfType(nodeType)
20+
return constrained ?? nodeType
21+
}

package.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,23 @@
1818
},
1919
"dependencies": {
2020
"@eslint-community/eslint-utils": "^4.4.1",
21+
"@typescript-eslint/utils": "^8.26.1",
2122
"enhanced-resolve": "^5.17.1",
2223
"eslint-plugin-es-x": "^7.8.0",
2324
"get-tsconfig": "^4.8.1",
2425
"globals": "^15.11.0",
2526
"ignore": "^5.3.2",
2627
"minimatch": "^9.0.5",
27-
"semver": "^7.6.3"
28+
"semver": "^7.6.3",
29+
"ts-declaration-location": "^1.0.6"
2830
},
2931
"devDependencies": {
3032
"@eslint/js": "^9.14.0",
3133
"@types/eslint": "^9.6.1",
3234
"@types/estree": "^1.0.6",
3335
"@types/node": "^20.17.5",
34-
"@typescript-eslint/parser": "^8.12.2",
35-
"@typescript-eslint/typescript-estree": "^8.12.2",
36+
"@typescript-eslint/parser": "^8.26.1",
37+
"@typescript-eslint/typescript-estree": "^8.26.1",
3638
"eslint": "^9.14.0",
3739
"eslint-config-prettier": "^9.1.0",
3840
"eslint-doc-generator": "^1.7.1",

tests/fixtures/no-sync/base/file.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// File needs to exists
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "ESNext",
5+
"moduleResolution": "Bundler",
6+
"strict": true,
7+
"skipLibCheck": true
8+
},
9+
"include": ["**/*"]
10+
}

tests/fixtures/no-sync/file.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// File needs to exists
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// File needs to exists
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "test",
3+
"version": "0.0.0",
4+
"dependencies": {
5+
"aaa": "0.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "ESNext",
5+
"moduleResolution": "Bundler",
6+
"strict": true,
7+
"skipLibCheck": true
8+
},
9+
"include": ["**/*"]
10+
}

tests/fixtures/no-sync/tsconfig.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "ESNext",
5+
"moduleResolution": "Bundler",
6+
"strict": true,
7+
"skipLibCheck": true
8+
},
9+
"include": ["**/*"]
10+
}

0 commit comments

Comments
 (0)