forked from mysticatea/eslint-plugin-node
-
-
Notifications
You must be signed in to change notification settings - Fork 54
feat!: rename rule shebang => hashbang, deprecate rule shebang #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.