Skip to content

Commit cefdb1c

Browse files
authoredMar 6, 2024··
feat!: rename rule shebang => hashbang, deprecate rule shebang (#198)
fixes #196
1 parent b383b49 commit cefdb1c

File tree

9 files changed

+788
-669
lines changed

9 files changed

+788
-669
lines changed
 

‎README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ For [Shareable Configs](https://eslint.org/docs/latest/developer-guide/shareable
114114
| [file-extension-in-import](docs/rules/file-extension-in-import.md) | enforce the style of file extensions in `import` declarations | | 🔧 | |
115115
| [global-require](docs/rules/global-require.md) | require `require()` calls to be placed at top-level module scope | | | |
116116
| [handle-callback-err](docs/rules/handle-callback-err.md) | require error handling in callbacks | | | |
117+
| [hashbang](docs/rules/hashbang.md) | require correct usage of hashbang | ☑️ 🟢 ✅ ☑️ 🟢 ✅ | 🔧 | |
117118
| [no-callback-literal](docs/rules/no-callback-literal.md) | enforce Node.js-style error-first callback pattern is followed | | | |
118119
| [no-deprecated-api](docs/rules/no-deprecated-api.md) | disallow deprecated APIs | ☑️ 🟢 ✅ ☑️ 🟢 ✅ | | |
119120
| [no-exports-assign](docs/rules/no-exports-assign.md) | disallow the assignment to `exports` | ☑️ 🟢 ✅ ☑️ 🟢 ✅ | | |
@@ -147,7 +148,7 @@ For [Shareable Configs](https://eslint.org/docs/latest/developer-guide/shareable
147148
| [prefer-promises/dns](docs/rules/prefer-promises/dns.md) | enforce `require("dns").promises` | | | |
148149
| [prefer-promises/fs](docs/rules/prefer-promises/fs.md) | enforce `require("fs").promises` | | | |
149150
| [process-exit-as-throw](docs/rules/process-exit-as-throw.md) | require that `process.exit()` expressions use the same code path as `throw` | ☑️ 🟢 ✅ ☑️ 🟢 ✅ | | |
150-
| [shebang](docs/rules/shebang.md) | require correct usage of shebang | ☑️ 🟢 ✅ ☑️ 🟢 ✅ | 🔧 | |
151+
| [shebang](docs/rules/shebang.md) | require correct usage of hashbang | | 🔧 | |
151152

152153
<!-- end auto-generated rules list -->
153154

‎docs/rules/hashbang.md

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Require correct usage of hashbang (`n/hashbang`)
2+
3+
💼 This rule is enabled in the following [configs](https://github.com/eslint-community/eslint-plugin-n#-configs): ☑️ `flat/recommended`, 🟢 `flat/recommended-module`, ✅ `flat/recommended-script`, ☑️ `recommended`, 🟢 `recommended-module`, ✅ `recommended-script`.
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
9+
When we make a CLI tool with Node.js, we add `bin` field to `package.json`, then we add a hashbang the entry file.
10+
This rule suggests correct usage of hashbang.
11+
12+
## 📖 Rule Details
13+
14+
This rule looks up `package.json` file from each linting target file.
15+
Starting from the directory of the target file, it goes up ancestor directories until found.
16+
17+
If `package.json` was not found, this rule does nothing.
18+
19+
This rule checks `bin` field of `package.json`, then if a target file matches one of `bin` files, it checks whether or not there is a correct hashbang.
20+
Otherwise it checks whether or not there is not a hashbang.
21+
22+
The following patterns are considered problems for files in `bin` field of `package.json`:
23+
24+
```js
25+
console.log("hello"); /*error This file needs hashbang "#!/usr/bin/env node".*/
26+
```
27+
28+
```js
29+
#!/usr/bin/env node /*error This file must not have Unicode BOM.*/
30+
console.log("hello");
31+
// If this file has Unicode BOM.
32+
```
33+
34+
```js
35+
#!/usr/bin/env node /*error This file must have Unix linebreaks (LF).*/
36+
console.log("hello");
37+
// If this file has Windows' linebreaks (CRLF).
38+
```
39+
40+
The following patterns are considered problems for other files:
41+
42+
```js
43+
#!/usr/bin/env node /*error This file needs no hashbang.*/
44+
console.log("hello");
45+
```
46+
47+
The following patterns are not considered problems for files in `bin` field of `package.json`:
48+
49+
```js
50+
#!/usr/bin/env node
51+
console.log("hello");
52+
```
53+
54+
The following patterns are not considered problems for other files:
55+
56+
```js
57+
console.log("hello");
58+
```
59+
60+
### Options
61+
62+
```json
63+
{
64+
"n/hashbang": ["error", {
65+
"convertPath": null,
66+
"ignoreUnpublished": false,
67+
"additionalExecutables": [],
68+
}]
69+
}
70+
```
71+
72+
#### convertPath
73+
74+
This can be configured in the rule options or as a shared setting [`settings.convertPath`](../shared-settings.md#convertpath).
75+
Please see the shared settings documentation for more information.
76+
77+
#### ignoreUnpublished
78+
79+
Allow for files that are not published to npm to be ignored by this rule.
80+
81+
#### additionalExecutables
82+
83+
Mark files as executable that are not referenced by the package.json#bin property
84+
85+
## 🔎 Implementation
86+
87+
- [Rule source](../../lib/rules/hashbang.js)
88+
- [Test source](../../tests/lib/rules/hashbang.js)

‎docs/rules/shebang.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Require correct usage of shebang (`n/shebang`)
1+
# Require correct usage of hashbang (`n/shebang`)
22

3-
💼 This rule is enabled in the following [configs](https://github.com/eslint-community/eslint-plugin-n#-configs): ☑️ `flat/recommended`, 🟢 `flat/recommended-module`, ✅ `flat/recommended-script`, ☑️ `recommended`, 🟢 `recommended-module`, ✅ `recommended-script`.
3+
This rule is deprecated. It was replaced by [`n/hashbang`](hashbang.md).
44

55
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
66

‎lib/configs/_commons.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ module.exports = {
1616
"n/no-unsupported-features/es-syntax": "error",
1717
"n/no-unsupported-features/node-builtins": "error",
1818
"n/process-exit-as-throw": "error",
19-
"n/shebang": "error",
19+
"n/hashbang": "error",
2020
},
2121
}

‎lib/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@ const rules = {
4343
"prefer-promises/dns": require("./rules/prefer-promises/dns"),
4444
"prefer-promises/fs": require("./rules/prefer-promises/fs"),
4545
"process-exit-as-throw": require("./rules/process-exit-as-throw"),
46-
shebang: require("./rules/shebang"),
46+
hashbang: require("./rules/hashbang"),
4747

4848
// Deprecated rules.
4949
"no-hide-core-modules": require("./rules/no-hide-core-modules"),
50+
shebang: require("./rules/shebang"),
5051
}
5152

5253
const mod = {

‎lib/rules/hashbang.js

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/**
2+
* @author Toru Nagashima
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
"use strict"
6+
7+
const path = require("path")
8+
const matcher = require("ignore")
9+
10+
const getConvertPath = require("../util/get-convert-path")
11+
const getPackageJson = require("../util/get-package-json")
12+
const getNpmignore = require("../util/get-npmignore")
13+
14+
const NODE_SHEBANG = "#!/usr/bin/env node\n"
15+
const SHEBANG_PATTERN = /^(#!.+?)?(\r)?\n/u
16+
const NODE_SHEBANG_PATTERN =
17+
/^#!\/usr\/bin\/env(?: -\S+)*(?: [^\s=-]+=\S+)* node(?: [^\r\n]+?)?\n/u
18+
19+
function simulateNodeResolutionAlgorithm(filePath, binField) {
20+
const possibilities = [filePath]
21+
let newFilePath = filePath.replace(/\.js$/u, "")
22+
possibilities.push(newFilePath)
23+
newFilePath = newFilePath.replace(/[/\\]index$/u, "")
24+
possibilities.push(newFilePath)
25+
return possibilities.includes(binField)
26+
}
27+
28+
/**
29+
* Checks whether or not a given path is a `bin` file.
30+
*
31+
* @param {string} filePath - A file path to check.
32+
* @param {string|object|undefined} binField - A value of the `bin` field of `package.json`.
33+
* @param {string} basedir - A directory path that `package.json` exists.
34+
* @returns {boolean} `true` if the file is a `bin` file.
35+
*/
36+
function isBinFile(filePath, binField, basedir) {
37+
if (!binField) {
38+
return false
39+
}
40+
if (typeof binField === "string") {
41+
return simulateNodeResolutionAlgorithm(
42+
filePath,
43+
path.resolve(basedir, binField)
44+
)
45+
}
46+
return Object.keys(binField).some(key =>
47+
simulateNodeResolutionAlgorithm(
48+
filePath,
49+
path.resolve(basedir, binField[key])
50+
)
51+
)
52+
}
53+
54+
/**
55+
* Gets the shebang line (includes a line ending) from a given code.
56+
*
57+
* @param {SourceCode} sourceCode - A source code object to check.
58+
* @returns {{length: number, bom: boolean, shebang: string, cr: boolean}}
59+
* shebang's information.
60+
* `retv.shebang` is an empty string if shebang doesn't exist.
61+
*/
62+
function getShebangInfo(sourceCode) {
63+
const m = SHEBANG_PATTERN.exec(sourceCode.text)
64+
65+
return {
66+
bom: sourceCode.hasBOM,
67+
cr: Boolean(m && m[2]),
68+
length: (m && m[0].length) || 0,
69+
shebang: (m && m[1] && `${m[1]}\n`) || "",
70+
}
71+
}
72+
73+
/** @type {import('eslint').Rule.RuleModule} */
74+
module.exports = {
75+
meta: {
76+
docs: {
77+
description: "require correct usage of hashbang",
78+
recommended: true,
79+
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/hashbang.md",
80+
},
81+
type: "problem",
82+
fixable: "code",
83+
schema: [
84+
{
85+
type: "object",
86+
properties: {
87+
convertPath: getConvertPath.schema,
88+
ignoreUnpublished: { type: "boolean" },
89+
additionalExecutables: {
90+
type: "array",
91+
items: { type: "string" },
92+
},
93+
},
94+
additionalProperties: false,
95+
},
96+
],
97+
messages: {
98+
unexpectedBOM: "This file must not have Unicode BOM.",
99+
expectedLF: "This file must have Unix linebreaks (LF).",
100+
expectedHashbangNode:
101+
'This file needs shebang "#!/usr/bin/env node".',
102+
expectedHashbang: "This file needs no shebang.",
103+
},
104+
},
105+
create(context) {
106+
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
107+
const filePath = context.filename ?? context.getFilename()
108+
if (filePath === "<input>") {
109+
return {}
110+
}
111+
112+
const p = getPackageJson(filePath)
113+
if (!p) {
114+
return {}
115+
}
116+
117+
const packageDirectory = path.dirname(p.filePath)
118+
119+
const originalAbsolutePath = path.resolve(filePath)
120+
const originalRelativePath = path
121+
.relative(packageDirectory, originalAbsolutePath)
122+
.replace(/\\/gu, "/")
123+
124+
const convertedRelativePath =
125+
getConvertPath(context)(originalRelativePath)
126+
const convertedAbsolutePath = path.resolve(
127+
packageDirectory,
128+
convertedRelativePath
129+
)
130+
131+
const { additionalExecutables = [] } = context.options?.[0] ?? {}
132+
133+
const executable = matcher()
134+
executable.add(additionalExecutables)
135+
const isExecutable = executable.test(convertedRelativePath)
136+
137+
if (
138+
(additionalExecutables.length === 0 ||
139+
isExecutable.ignored === false) &&
140+
context.options?.[0]?.ignoreUnpublished === true
141+
) {
142+
const npmignore = getNpmignore(convertedAbsolutePath)
143+
144+
if (npmignore.match(convertedRelativePath)) {
145+
return {}
146+
}
147+
}
148+
149+
const needsShebang =
150+
isExecutable.ignored === true ||
151+
isBinFile(convertedAbsolutePath, p.bin, packageDirectory)
152+
const info = getShebangInfo(sourceCode)
153+
154+
return {
155+
Program() {
156+
const loc = {
157+
start: { line: 1, column: 0 },
158+
end: { line: 1, column: sourceCode.lines.at(0).length },
159+
}
160+
161+
if (
162+
needsShebang
163+
? NODE_SHEBANG_PATTERN.test(info.shebang)
164+
: !info.shebang
165+
) {
166+
// Good the shebang target.
167+
// Checks BOM and \r.
168+
if (needsShebang && info.bom) {
169+
context.report({
170+
loc,
171+
messageId: "unexpectedBOM",
172+
fix(fixer) {
173+
return fixer.removeRange([-1, 0])
174+
},
175+
})
176+
}
177+
if (needsShebang && info.cr) {
178+
context.report({
179+
loc,
180+
messageId: "expectedLF",
181+
fix(fixer) {
182+
const index = sourceCode.text.indexOf("\r")
183+
return fixer.removeRange([index, index + 1])
184+
},
185+
})
186+
}
187+
} else if (needsShebang) {
188+
// Shebang is lacking.
189+
context.report({
190+
loc,
191+
messageId: "expectedHashbangNode",
192+
fix(fixer) {
193+
return fixer.replaceTextRange(
194+
[-1, info.length],
195+
NODE_SHEBANG
196+
)
197+
},
198+
})
199+
} else {
200+
// Shebang is extra.
201+
context.report({
202+
loc,
203+
messageId: "expectedHashbang",
204+
fix(fixer) {
205+
return fixer.removeRange([0, info.length])
206+
},
207+
})
208+
}
209+
},
210+
}
211+
},
212+
}

‎lib/rules/shebang.js

+9-203
Original file line numberDiff line numberDiff line change
@@ -1,212 +1,18 @@
11
/**
2-
* @author Toru Nagashima
3-
* See LICENSE file in root directory for full license.
2+
* @fileoverview the rule has been renamed to `hashbang`. Please use `hashbang` instead.
3+
* @deprecated
4+
* @author 唯然<weiran.zsd@outlook.com>
45
*/
56
"use strict"
67

7-
const path = require("path")
8-
const matcher = require("ignore")
8+
const hashbang = require("./hashbang.js")
99

10-
const getConvertPath = require("../util/get-convert-path")
11-
const getPackageJson = require("../util/get-package-json")
12-
const getNpmignore = require("../util/get-npmignore")
13-
14-
const NODE_SHEBANG = "#!/usr/bin/env node\n"
15-
const SHEBANG_PATTERN = /^(#!.+?)?(\r)?\n/u
16-
const NODE_SHEBANG_PATTERN =
17-
/^#!\/usr\/bin\/env(?: -\S+)*(?: [^\s=-]+=\S+)* node(?: [^\r\n]+?)?\n/u
18-
19-
function simulateNodeResolutionAlgorithm(filePath, binField) {
20-
const possibilities = [filePath]
21-
let newFilePath = filePath.replace(/\.js$/u, "")
22-
possibilities.push(newFilePath)
23-
newFilePath = newFilePath.replace(/[/\\]index$/u, "")
24-
possibilities.push(newFilePath)
25-
return possibilities.includes(binField)
26-
}
27-
28-
/**
29-
* Checks whether or not a given path is a `bin` file.
30-
*
31-
* @param {string} filePath - A file path to check.
32-
* @param {string|object|undefined} binField - A value of the `bin` field of `package.json`.
33-
* @param {string} basedir - A directory path that `package.json` exists.
34-
* @returns {boolean} `true` if the file is a `bin` file.
35-
*/
36-
function isBinFile(filePath, binField, basedir) {
37-
if (!binField) {
38-
return false
39-
}
40-
if (typeof binField === "string") {
41-
return simulateNodeResolutionAlgorithm(
42-
filePath,
43-
path.resolve(basedir, binField)
44-
)
45-
}
46-
return Object.keys(binField).some(key =>
47-
simulateNodeResolutionAlgorithm(
48-
filePath,
49-
path.resolve(basedir, binField[key])
50-
)
51-
)
52-
}
53-
54-
/**
55-
* Gets the shebang line (includes a line ending) from a given code.
56-
*
57-
* @param {SourceCode} sourceCode - A source code object to check.
58-
* @returns {{length: number, bom: boolean, shebang: string, cr: boolean}}
59-
* shebang's information.
60-
* `retv.shebang` is an empty string if shebang doesn't exist.
61-
*/
62-
function getShebangInfo(sourceCode) {
63-
const m = SHEBANG_PATTERN.exec(sourceCode.text)
64-
65-
return {
66-
bom: sourceCode.hasBOM,
67-
cr: Boolean(m && m[2]),
68-
length: (m && m[0].length) || 0,
69-
shebang: (m && m[1] && `${m[1]}\n`) || "",
70-
}
71-
}
72-
73-
/** @type {import('eslint').Rule.RuleModule} */
7410
module.exports = {
7511
meta: {
76-
docs: {
77-
description: "require correct usage of shebang",
78-
recommended: true,
79-
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/shebang.md",
80-
},
81-
type: "problem",
82-
fixable: "code",
83-
schema: [
84-
{
85-
type: "object",
86-
properties: {
87-
convertPath: getConvertPath.schema,
88-
ignoreUnpublished: { type: "boolean" },
89-
additionalExecutables: {
90-
type: "array",
91-
items: { type: "string" },
92-
},
93-
},
94-
additionalProperties: false,
95-
},
96-
],
97-
messages: {
98-
unexpectedBOM: "This file must not have Unicode BOM.",
99-
expectedLF: "This file must have Unix linebreaks (LF).",
100-
expectedHashbangNode:
101-
'This file needs shebang "#!/usr/bin/env node".',
102-
expectedHashbang: "This file needs no shebang.",
103-
},
104-
},
105-
create(context) {
106-
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
107-
const filePath = context.filename ?? context.getFilename()
108-
if (filePath === "<input>") {
109-
return {}
110-
}
111-
112-
const p = getPackageJson(filePath)
113-
if (!p) {
114-
return {}
115-
}
116-
117-
const packageDirectory = path.dirname(p.filePath)
118-
119-
const originalAbsolutePath = path.resolve(filePath)
120-
const originalRelativePath = path
121-
.relative(packageDirectory, originalAbsolutePath)
122-
.replace(/\\/gu, "/")
123-
124-
const convertedRelativePath =
125-
getConvertPath(context)(originalRelativePath)
126-
const convertedAbsolutePath = path.resolve(
127-
packageDirectory,
128-
convertedRelativePath
129-
)
130-
131-
const { additionalExecutables = [] } = context.options?.[0] ?? {}
132-
133-
const executable = matcher()
134-
executable.add(additionalExecutables)
135-
const isExecutable = executable.test(convertedRelativePath)
136-
137-
if (
138-
(additionalExecutables.length === 0 ||
139-
isExecutable.ignored === false) &&
140-
context.options?.[0]?.ignoreUnpublished === true
141-
) {
142-
const npmignore = getNpmignore(convertedAbsolutePath)
143-
144-
if (npmignore.match(convertedRelativePath)) {
145-
return {}
146-
}
147-
}
148-
149-
const needsShebang =
150-
isExecutable.ignored === true ||
151-
isBinFile(convertedAbsolutePath, p.bin, packageDirectory)
152-
const info = getShebangInfo(sourceCode)
153-
154-
return {
155-
Program() {
156-
const loc = {
157-
start: { line: 1, column: 0 },
158-
end: { line: 1, column: sourceCode.lines.at(0).length },
159-
}
160-
161-
if (
162-
needsShebang
163-
? NODE_SHEBANG_PATTERN.test(info.shebang)
164-
: !info.shebang
165-
) {
166-
// Good the shebang target.
167-
// Checks BOM and \r.
168-
if (needsShebang && info.bom) {
169-
context.report({
170-
loc,
171-
messageId: "unexpectedBOM",
172-
fix(fixer) {
173-
return fixer.removeRange([-1, 0])
174-
},
175-
})
176-
}
177-
if (needsShebang && info.cr) {
178-
context.report({
179-
loc,
180-
messageId: "expectedLF",
181-
fix(fixer) {
182-
const index = sourceCode.text.indexOf("\r")
183-
return fixer.removeRange([index, index + 1])
184-
},
185-
})
186-
}
187-
} else if (needsShebang) {
188-
// Shebang is lacking.
189-
context.report({
190-
loc,
191-
messageId: "expectedHashbangNode",
192-
fix(fixer) {
193-
return fixer.replaceTextRange(
194-
[-1, info.length],
195-
NODE_SHEBANG
196-
)
197-
},
198-
})
199-
} else {
200-
// Shebang is extra.
201-
context.report({
202-
loc,
203-
messageId: "expectedHashbang",
204-
fix(fixer) {
205-
return fixer.removeRange([0, info.length])
206-
},
207-
})
208-
}
209-
},
210-
}
12+
...hashbang.meta,
13+
deprecated: true,
14+
replacedBy: ["n/hashbang"],
15+
docs: { ...hashbang.meta.docs, recommended: false },
21116
},
17+
create: hashbang.create,
21218
}

‎tests/lib/rules/hashbang.js

+465
Large diffs are not rendered by default.

‎tests/lib/rules/shebang.js

+7-461
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.