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 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"),
@@ -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: { type: "string" },
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
- if (ignores.includes(node.name)) {
69
- return
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.18.0",
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.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",
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
+ }