Skip to content

Commit 5544f20

Browse files
feat: add support for ignoring sync methods from certain locations (#392)
Co-authored-by: Sebastian Good <2230835+scagood@users.noreply.github.com>
1 parent 4efe60f commit 5544f20

File tree

18 files changed

+543
-9
lines changed

18 files changed

+543
-9
lines changed

docs/rules/no-sync.md

Lines changed: 57 additions & 0 deletions
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

Lines changed: 110 additions & 4 deletions
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

Lines changed: 47 additions & 0 deletions
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

Lines changed: 24 additions & 0 deletions
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

Lines changed: 21 additions & 0 deletions
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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,23 @@
1717
},
1818
"dependencies": {
1919
"@eslint-community/eslint-utils": "^4.4.1",
20+
"@typescript-eslint/utils": "^8.21.0",
2021
"enhanced-resolve": "^5.17.1",
2122
"eslint-plugin-es-x": "^7.8.0",
2223
"get-tsconfig": "^4.8.1",
2324
"globals": "^15.11.0",
2425
"ignore": "^5.3.2",
2526
"minimatch": "^9.0.5",
26-
"semver": "^7.6.3"
27+
"semver": "^7.6.3",
28+
"ts-declaration-location": "^1.0.5"
2729
},
2830
"devDependencies": {
2931
"@eslint/js": "^9.14.0",
3032
"@types/eslint": "^9.6.1",
3133
"@types/estree": "^1.0.6",
3234
"@types/node": "^20.17.5",
33-
"@typescript-eslint/parser": "^8.12.2",
34-
"@typescript-eslint/typescript-estree": "^8.12.2",
35+
"@typescript-eslint/parser": "^8.21.0",
36+
"@typescript-eslint/typescript-estree": "^8.21.0",
3537
"eslint": "^9.14.0",
3638
"eslint-config-prettier": "^9.1.0",
3739
"eslint-doc-generator": "^1.7.1",
@@ -51,7 +53,7 @@
5153
"rimraf": "^5.0.10",
5254
"ts-ignore-import": "^4.0.1",
5355
"type-fest": "^4.26.1",
54-
"typescript": "~5.6"
56+
"typescript": "^5.7"
5557
},
5658
"scripts": {
5759
"build": "node scripts/update",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// File needs to exists
Lines changed: 10 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// File needs to exists
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// File needs to exists

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy