eslint-plugin-n 17.18.0 → 17.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/lib/index.js +1 -0
- package/lib/rules/no-sync.js +125 -5
- package/lib/rules/no-top-level-await.js +132 -0
- package/lib/util/get-full-type-name.js +47 -0
- package/lib/util/get-parser-services.js +24 -0
- package/lib/util/get-type-of-node.js +21 -0
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -156,6 +156,7 @@ For [Shareable Configs](https://eslint.org/docs/latest/developer-guide/shareable
|
|
|
156
156
|
| [no-restricted-import](docs/rules/no-restricted-import.md) | disallow specified modules when loaded by `import` declarations | | | |
|
|
157
157
|
| [no-restricted-require](docs/rules/no-restricted-require.md) | disallow specified modules when loaded by `require` | | | |
|
|
158
158
|
| [no-sync](docs/rules/no-sync.md) | disallow synchronous methods | | | |
|
|
159
|
+
| [no-top-level-await](docs/rules/no-top-level-await.md) | disallow top-level `await` in published modules | | | |
|
|
159
160
|
| [no-unpublished-bin](docs/rules/no-unpublished-bin.md) | disallow `bin` files that npm ignores | 🟢 ✅ | | |
|
|
160
161
|
| [no-unpublished-import](docs/rules/no-unpublished-import.md) | disallow `import` declarations which import private modules | 🟢 ✅ | | |
|
|
161
162
|
| [no-unpublished-require](docs/rules/no-unpublished-require.md) | disallow `require()` expressions which import private modules | 🟢 ✅ | | |
|
package/lib/index.js
CHANGED
|
@@ -34,6 +34,7 @@ const base = {
|
|
|
34
34
|
"no-restricted-import": require("./rules/no-restricted-import"),
|
|
35
35
|
"no-restricted-require": require("./rules/no-restricted-require"),
|
|
36
36
|
"no-sync": require("./rules/no-sync"),
|
|
37
|
+
"no-top-level-await": require("./rules/no-top-level-await"),
|
|
37
38
|
"no-unpublished-bin": require("./rules/no-unpublished-bin"),
|
|
38
39
|
"no-unpublished-import": require("./rules/no-unpublished-import"),
|
|
39
40
|
"no-unpublished-require": require("./rules/no-unpublished-require"),
|
package/lib/rules/no-sync.js
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
"use strict"
|
|
6
6
|
|
|
7
|
+
let typeMatchesSpecifier =
|
|
8
|
+
/** @type {import('ts-declaration-location').default | undefined} */
|
|
9
|
+
(undefined)
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
typeMatchesSpecifier =
|
|
13
|
+
/** @type {import('ts-declaration-location').default} */ (
|
|
14
|
+
/** @type {unknown} */ (require("ts-declaration-location"))
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line no-empty -- Deliberately left empty.
|
|
18
|
+
} catch {}
|
|
19
|
+
const getTypeOfNode = require("../util/get-type-of-node")
|
|
20
|
+
const getParserServices = require("../util/get-parser-services")
|
|
21
|
+
const getFullTypeName = require("../util/get-full-type-name")
|
|
22
|
+
|
|
7
23
|
const selectors = [
|
|
8
24
|
// fs.readFileSync()
|
|
9
25
|
// readFileSync.call(null, 'path')
|
|
@@ -16,7 +32,7 @@ const selectors = [
|
|
|
16
32
|
* @typedef {[
|
|
17
33
|
* {
|
|
18
34
|
* allowAtRootLevel?: boolean
|
|
19
|
-
* ignores?: string[]
|
|
35
|
+
* ignores?: (string | { from: "file"; path?: string; name?: string[]; } | { from: "package"; package?: string; name?: string[]; } | { from: "lib"; name?: string[]; })[]
|
|
20
36
|
* }?
|
|
21
37
|
* ]} RuleOptions
|
|
22
38
|
*/
|
|
@@ -40,7 +56,56 @@ module.exports = {
|
|
|
40
56
|
},
|
|
41
57
|
ignores: {
|
|
42
58
|
type: "array",
|
|
43
|
-
items: {
|
|
59
|
+
items: {
|
|
60
|
+
oneOf: [
|
|
61
|
+
{ type: "string" },
|
|
62
|
+
{
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
from: { const: "file" },
|
|
66
|
+
path: {
|
|
67
|
+
type: "string",
|
|
68
|
+
},
|
|
69
|
+
name: {
|
|
70
|
+
type: "array",
|
|
71
|
+
items: {
|
|
72
|
+
type: "string",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
from: { const: "lib" },
|
|
82
|
+
name: {
|
|
83
|
+
type: "array",
|
|
84
|
+
items: {
|
|
85
|
+
type: "string",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
additionalProperties: false,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
from: { const: "package" },
|
|
95
|
+
package: {
|
|
96
|
+
type: "string",
|
|
97
|
+
},
|
|
98
|
+
name: {
|
|
99
|
+
type: "array",
|
|
100
|
+
items: {
|
|
101
|
+
type: "string",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
additionalProperties: false,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
44
109
|
default: [],
|
|
45
110
|
},
|
|
46
111
|
},
|
|
@@ -65,15 +130,70 @@ module.exports = {
|
|
|
65
130
|
* @returns {void}
|
|
66
131
|
*/
|
|
67
132
|
[selector.join(",")](node) {
|
|
68
|
-
|
|
69
|
-
|
|
133
|
+
const parserServices = getParserServices(context)
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @type {import('typescript').Type | undefined | null}
|
|
137
|
+
*/
|
|
138
|
+
let type = undefined
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @type {string | undefined | null}
|
|
142
|
+
*/
|
|
143
|
+
let fullName = undefined
|
|
144
|
+
|
|
145
|
+
for (const ignore of ignores) {
|
|
146
|
+
if (typeof ignore === "string") {
|
|
147
|
+
if (ignore === node.name) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
parserServices === null ||
|
|
156
|
+
parserServices.program === null
|
|
157
|
+
) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
'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.'
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (typeMatchesSpecifier === undefined) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
'ts-declaration-location not available. Rule "n/no-sync" is configured to use "ignores" option with a non-string value. This requires ts-declaration-location to be available.'
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
type =
|
|
170
|
+
type === undefined
|
|
171
|
+
? getTypeOfNode(node, parserServices)
|
|
172
|
+
: type
|
|
173
|
+
|
|
174
|
+
fullName =
|
|
175
|
+
fullName === undefined
|
|
176
|
+
? getFullTypeName(type)
|
|
177
|
+
: fullName
|
|
178
|
+
|
|
179
|
+
if (
|
|
180
|
+
typeMatchesSpecifier(
|
|
181
|
+
parserServices.program,
|
|
182
|
+
ignore,
|
|
183
|
+
type
|
|
184
|
+
) &&
|
|
185
|
+
(ignore.name === undefined ||
|
|
186
|
+
ignore.name.includes(fullName ?? node.name))
|
|
187
|
+
) {
|
|
188
|
+
return
|
|
189
|
+
}
|
|
70
190
|
}
|
|
71
191
|
|
|
72
192
|
context.report({
|
|
73
193
|
node: node.parent,
|
|
74
194
|
messageId: "noSync",
|
|
75
195
|
data: {
|
|
76
|
-
propertyName: node.name,
|
|
196
|
+
propertyName: fullName ?? node.name,
|
|
77
197
|
},
|
|
78
198
|
})
|
|
79
199
|
},
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author Yosuke Ota <https://github.com/ota-meshi>
|
|
3
|
+
* See LICENSE file in root directory for full license.
|
|
4
|
+
*/
|
|
5
|
+
"use strict"
|
|
6
|
+
|
|
7
|
+
const path = require("path")
|
|
8
|
+
const { getSourceCode } = require("../util/eslint-compat")
|
|
9
|
+
const getConvertPath = require("../util/get-convert-path")
|
|
10
|
+
const { getPackageJson } = require("../util/get-package-json")
|
|
11
|
+
const { isBinFile } = require("../util/is-bin-file")
|
|
12
|
+
const getNpmignore = require("../util/get-npmignore")
|
|
13
|
+
|
|
14
|
+
const HASHBANG_ENV = "#!/usr/bin/env"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {[
|
|
18
|
+
* {
|
|
19
|
+
* ignoreBin?:boolean
|
|
20
|
+
* convertPath?: import('../util/get-convert-path').ConvertPath;
|
|
21
|
+
* }?
|
|
22
|
+
* ]} RuleOptions
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Checks whether the code has a hashbang comment or not.
|
|
27
|
+
* @param {import('./rule-module').PluginRuleContext<{RuleOptions: RuleOptions}>} context
|
|
28
|
+
*/
|
|
29
|
+
function hasHashbang(context) {
|
|
30
|
+
const sourceCode = getSourceCode(context)
|
|
31
|
+
return Boolean(sourceCode.text.startsWith(HASHBANG_ENV))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @param {import('./rule-module').PluginRuleContext<{RuleOptions: RuleOptions}>} context */
|
|
35
|
+
function ignore(context) {
|
|
36
|
+
const options = context.options[0] || {}
|
|
37
|
+
const ignoreBin = options.ignoreBin ?? false
|
|
38
|
+
if (ignoreBin && hasHashbang(context)) {
|
|
39
|
+
// If the code has a hashbang comment, it is considered an executable file.
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const filePath = context.filename ?? context.getFilename()
|
|
44
|
+
if (filePath === "<input>") {
|
|
45
|
+
// The file path is "<input>" (not specified), so it will be ignored.
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
const originalAbsolutePath = path.resolve(filePath)
|
|
49
|
+
|
|
50
|
+
// Find package.json
|
|
51
|
+
const packageJson = getPackageJson(originalAbsolutePath)
|
|
52
|
+
if (typeof packageJson?.filePath !== "string") {
|
|
53
|
+
// The file is not in a package, so it will be ignored.
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Convert by convertPath option
|
|
58
|
+
const packageDirectory = path.dirname(packageJson.filePath)
|
|
59
|
+
const convertedRelativePath = getConvertPath(context)(
|
|
60
|
+
path
|
|
61
|
+
.relative(packageDirectory, originalAbsolutePath)
|
|
62
|
+
.replace(/\\/gu, "/")
|
|
63
|
+
)
|
|
64
|
+
const convertedAbsolutePath = path.resolve(
|
|
65
|
+
packageDirectory,
|
|
66
|
+
convertedRelativePath
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
ignoreBin &&
|
|
71
|
+
isBinFile(convertedAbsolutePath, packageJson.bin, packageDirectory)
|
|
72
|
+
) {
|
|
73
|
+
// The file is defined in the `bin` field of `package.json`
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check ignored or not
|
|
78
|
+
const npmignore = getNpmignore(convertedAbsolutePath)
|
|
79
|
+
if (npmignore.match(convertedRelativePath)) {
|
|
80
|
+
// The file is unpublished file, so it will be ignored.
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @type {import('./rule-module').RuleModule<{RuleOptions: RuleOptions}>} */
|
|
88
|
+
module.exports = {
|
|
89
|
+
meta: {
|
|
90
|
+
docs: {
|
|
91
|
+
description: "disallow top-level `await` in published modules",
|
|
92
|
+
recommended: false,
|
|
93
|
+
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-top-level-await.md",
|
|
94
|
+
},
|
|
95
|
+
fixable: null,
|
|
96
|
+
messages: {
|
|
97
|
+
forbidden: "Top-level `await` is forbidden in published modules.",
|
|
98
|
+
},
|
|
99
|
+
schema: [
|
|
100
|
+
{
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
ignoreBin: { type: "boolean" },
|
|
104
|
+
convertPath: getConvertPath.schema,
|
|
105
|
+
},
|
|
106
|
+
additionalProperties: false,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
type: "problem",
|
|
110
|
+
},
|
|
111
|
+
create(context) {
|
|
112
|
+
if (ignore(context)) {
|
|
113
|
+
return {}
|
|
114
|
+
}
|
|
115
|
+
let functionDepth = 0
|
|
116
|
+
return {
|
|
117
|
+
":function"() {
|
|
118
|
+
functionDepth++
|
|
119
|
+
},
|
|
120
|
+
":function:exit"() {
|
|
121
|
+
functionDepth--
|
|
122
|
+
},
|
|
123
|
+
"AwaitExpression, ForOfStatement[await=true]"(node) {
|
|
124
|
+
if (functionDepth > 0) {
|
|
125
|
+
// not top-level
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
context.report({ node, messageId: "forbidden" })
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-n",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.19.0",
|
|
4
4
|
"description": "Additional ESLint's rules for Node.js",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
@@ -18,21 +18,23 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@eslint-community/eslint-utils": "^4.5.0",
|
|
21
|
+
"@typescript-eslint/utils": "^8.26.1",
|
|
21
22
|
"enhanced-resolve": "^5.17.1",
|
|
22
23
|
"eslint-plugin-es-x": "^7.8.0",
|
|
23
24
|
"get-tsconfig": "^4.8.1",
|
|
24
25
|
"globals": "^15.11.0",
|
|
25
26
|
"ignore": "^5.3.2",
|
|
26
27
|
"minimatch": "^9.0.5",
|
|
27
|
-
"semver": "^7.6.3"
|
|
28
|
+
"semver": "^7.6.3",
|
|
29
|
+
"ts-declaration-location": "^1.0.6"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
32
|
"@eslint/js": "^9.14.0",
|
|
31
33
|
"@types/eslint": "^9.6.1",
|
|
32
34
|
"@types/estree": "^1.0.6",
|
|
33
35
|
"@types/node": "^20.17.5",
|
|
34
|
-
"@typescript-eslint/parser": "^8.
|
|
35
|
-
"@typescript-eslint/typescript-estree": "^8.
|
|
36
|
+
"@typescript-eslint/parser": "^8.26.1",
|
|
37
|
+
"@typescript-eslint/typescript-estree": "^8.26.1",
|
|
36
38
|
"eslint": "^9.14.0",
|
|
37
39
|
"eslint-config-prettier": "^9.1.0",
|
|
38
40
|
"eslint-doc-generator": "^1.7.1",
|
|
@@ -120,4 +122,4 @@
|
|
|
120
122
|
"imports": {
|
|
121
123
|
"#test-helpers": "./tests/test-helpers.js"
|
|
122
124
|
}
|
|
123
|
-
}
|
|
125
|
+
}
|