forked from mysticatea/eslint-plugin-node
-
-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: rename rule shebang => hashbang, deprecate rule shebang (#198)
fixes #196
- Loading branch information
1 parent
b383b49
commit cefdb1c
Showing
9 changed files
with
788 additions
and
669 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# Require correct usage of hashbang (`n/hashbang`) | ||
|
||
💼 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`. | ||
|
||
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). | ||
|
||
<!-- end auto-generated rule header --> | ||
|
||
When we make a CLI tool with Node.js, we add `bin` field to `package.json`, then we add a hashbang the entry file. | ||
This rule suggests correct usage of hashbang. | ||
|
||
## 📖 Rule Details | ||
|
||
This rule looks up `package.json` file from each linting target file. | ||
Starting from the directory of the target file, it goes up ancestor directories until found. | ||
|
||
If `package.json` was not found, this rule does nothing. | ||
|
||
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. | ||
Otherwise it checks whether or not there is not a hashbang. | ||
|
||
The following patterns are considered problems for files in `bin` field of `package.json`: | ||
|
||
```js | ||
console.log("hello"); /*error This file needs hashbang "#!/usr/bin/env node".*/ | ||
``` | ||
|
||
```js | ||
#!/usr/bin/env node /*error This file must not have Unicode BOM.*/ | ||
console.log("hello"); | ||
// If this file has Unicode BOM. | ||
``` | ||
|
||
```js | ||
#!/usr/bin/env node /*error This file must have Unix linebreaks (LF).*/ | ||
console.log("hello"); | ||
// If this file has Windows' linebreaks (CRLF). | ||
``` | ||
|
||
The following patterns are considered problems for other files: | ||
|
||
```js | ||
#!/usr/bin/env node /*error This file needs no hashbang.*/ | ||
console.log("hello"); | ||
``` | ||
|
||
The following patterns are not considered problems for files in `bin` field of `package.json`: | ||
|
||
```js | ||
#!/usr/bin/env node | ||
console.log("hello"); | ||
``` | ||
|
||
The following patterns are not considered problems for other files: | ||
|
||
```js | ||
console.log("hello"); | ||
``` | ||
|
||
### Options | ||
|
||
```json | ||
{ | ||
"n/hashbang": ["error", { | ||
"convertPath": null, | ||
"ignoreUnpublished": false, | ||
"additionalExecutables": [], | ||
}] | ||
} | ||
``` | ||
|
||
#### convertPath | ||
|
||
This can be configured in the rule options or as a shared setting [`settings.convertPath`](../shared-settings.md#convertpath). | ||
Please see the shared settings documentation for more information. | ||
|
||
#### ignoreUnpublished | ||
|
||
Allow for files that are not published to npm to be ignored by this rule. | ||
|
||
#### additionalExecutables | ||
|
||
Mark files as executable that are not referenced by the package.json#bin property | ||
|
||
## 🔎 Implementation | ||
|
||
- [Rule source](../../lib/rules/hashbang.js) | ||
- [Test source](../../tests/lib/rules/hashbang.js) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
/** | ||
* @author Toru Nagashima | ||
* See LICENSE file in root directory for full license. | ||
*/ | ||
"use strict" | ||
|
||
const path = require("path") | ||
const matcher = require("ignore") | ||
|
||
const getConvertPath = require("../util/get-convert-path") | ||
const getPackageJson = require("../util/get-package-json") | ||
const getNpmignore = require("../util/get-npmignore") | ||
|
||
const NODE_SHEBANG = "#!/usr/bin/env node\n" | ||
const SHEBANG_PATTERN = /^(#!.+?)?(\r)?\n/u | ||
const NODE_SHEBANG_PATTERN = | ||
/^#!\/usr\/bin\/env(?: -\S+)*(?: [^\s=-]+=\S+)* node(?: [^\r\n]+?)?\n/u | ||
|
||
function simulateNodeResolutionAlgorithm(filePath, binField) { | ||
const possibilities = [filePath] | ||
let newFilePath = filePath.replace(/\.js$/u, "") | ||
possibilities.push(newFilePath) | ||
newFilePath = newFilePath.replace(/[/\\]index$/u, "") | ||
possibilities.push(newFilePath) | ||
return possibilities.includes(binField) | ||
} | ||
|
||
/** | ||
* Checks whether or not a given path is a `bin` file. | ||
* | ||
* @param {string} filePath - A file path to check. | ||
* @param {string|object|undefined} binField - A value of the `bin` field of `package.json`. | ||
* @param {string} basedir - A directory path that `package.json` exists. | ||
* @returns {boolean} `true` if the file is a `bin` file. | ||
*/ | ||
function isBinFile(filePath, binField, basedir) { | ||
if (!binField) { | ||
return false | ||
} | ||
if (typeof binField === "string") { | ||
return simulateNodeResolutionAlgorithm( | ||
filePath, | ||
path.resolve(basedir, binField) | ||
) | ||
} | ||
return Object.keys(binField).some(key => | ||
simulateNodeResolutionAlgorithm( | ||
filePath, | ||
path.resolve(basedir, binField[key]) | ||
) | ||
) | ||
} | ||
|
||
/** | ||
* Gets the shebang line (includes a line ending) from a given code. | ||
* | ||
* @param {SourceCode} sourceCode - A source code object to check. | ||
* @returns {{length: number, bom: boolean, shebang: string, cr: boolean}} | ||
* shebang's information. | ||
* `retv.shebang` is an empty string if shebang doesn't exist. | ||
*/ | ||
function getShebangInfo(sourceCode) { | ||
const m = SHEBANG_PATTERN.exec(sourceCode.text) | ||
|
||
return { | ||
bom: sourceCode.hasBOM, | ||
cr: Boolean(m && m[2]), | ||
length: (m && m[0].length) || 0, | ||
shebang: (m && m[1] && `${m[1]}\n`) || "", | ||
} | ||
} | ||
|
||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
meta: { | ||
docs: { | ||
description: "require correct usage of hashbang", | ||
recommended: true, | ||
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/hashbang.md", | ||
}, | ||
type: "problem", | ||
fixable: "code", | ||
schema: [ | ||
{ | ||
type: "object", | ||
properties: { | ||
convertPath: getConvertPath.schema, | ||
ignoreUnpublished: { type: "boolean" }, | ||
additionalExecutables: { | ||
type: "array", | ||
items: { type: "string" }, | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
messages: { | ||
unexpectedBOM: "This file must not have Unicode BOM.", | ||
expectedLF: "This file must have Unix linebreaks (LF).", | ||
expectedHashbangNode: | ||
'This file needs shebang "#!/usr/bin/env node".', | ||
expectedHashbang: "This file needs no shebang.", | ||
}, | ||
}, | ||
create(context) { | ||
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 | ||
const filePath = context.filename ?? context.getFilename() | ||
if (filePath === "<input>") { | ||
return {} | ||
} | ||
|
||
const p = getPackageJson(filePath) | ||
if (!p) { | ||
return {} | ||
} | ||
|
||
const packageDirectory = path.dirname(p.filePath) | ||
|
||
const originalAbsolutePath = path.resolve(filePath) | ||
const originalRelativePath = path | ||
.relative(packageDirectory, originalAbsolutePath) | ||
.replace(/\\/gu, "/") | ||
|
||
const convertedRelativePath = | ||
getConvertPath(context)(originalRelativePath) | ||
const convertedAbsolutePath = path.resolve( | ||
packageDirectory, | ||
convertedRelativePath | ||
) | ||
|
||
const { additionalExecutables = [] } = context.options?.[0] ?? {} | ||
|
||
const executable = matcher() | ||
executable.add(additionalExecutables) | ||
const isExecutable = executable.test(convertedRelativePath) | ||
|
||
if ( | ||
(additionalExecutables.length === 0 || | ||
isExecutable.ignored === false) && | ||
context.options?.[0]?.ignoreUnpublished === true | ||
) { | ||
const npmignore = getNpmignore(convertedAbsolutePath) | ||
|
||
if (npmignore.match(convertedRelativePath)) { | ||
return {} | ||
} | ||
} | ||
|
||
const needsShebang = | ||
isExecutable.ignored === true || | ||
isBinFile(convertedAbsolutePath, p.bin, packageDirectory) | ||
const info = getShebangInfo(sourceCode) | ||
|
||
return { | ||
Program() { | ||
const loc = { | ||
start: { line: 1, column: 0 }, | ||
end: { line: 1, column: sourceCode.lines.at(0).length }, | ||
} | ||
|
||
if ( | ||
needsShebang | ||
? NODE_SHEBANG_PATTERN.test(info.shebang) | ||
: !info.shebang | ||
) { | ||
// Good the shebang target. | ||
// Checks BOM and \r. | ||
if (needsShebang && info.bom) { | ||
context.report({ | ||
loc, | ||
messageId: "unexpectedBOM", | ||
fix(fixer) { | ||
return fixer.removeRange([-1, 0]) | ||
}, | ||
}) | ||
} | ||
if (needsShebang && info.cr) { | ||
context.report({ | ||
loc, | ||
messageId: "expectedLF", | ||
fix(fixer) { | ||
const index = sourceCode.text.indexOf("\r") | ||
return fixer.removeRange([index, index + 1]) | ||
}, | ||
}) | ||
} | ||
} else if (needsShebang) { | ||
// Shebang is lacking. | ||
context.report({ | ||
loc, | ||
messageId: "expectedHashbangNode", | ||
fix(fixer) { | ||
return fixer.replaceTextRange( | ||
[-1, info.length], | ||
NODE_SHEBANG | ||
) | ||
}, | ||
}) | ||
} else { | ||
// Shebang is extra. | ||
context.report({ | ||
loc, | ||
messageId: "expectedHashbang", | ||
fix(fixer) { | ||
return fixer.removeRange([0, info.length]) | ||
}, | ||
}) | ||
} | ||
}, | ||
} | ||
}, | ||
} |
Oops, something went wrong.