@eslint-sh/eslint-plugin 0.5.0 → 0.6.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 +223 -17
- package/dist/command-harvest/builtins/bash.d.ts +36 -0
- package/dist/command-harvest/builtins/bash.js +98 -0
- package/dist/command-harvest/builtins/bats.d.ts +45 -0
- package/dist/command-harvest/builtins/bats.js +33 -0
- package/dist/command-harvest/builtins/index.d.ts +18 -0
- package/dist/command-harvest/builtins/index.js +39 -0
- package/dist/command-harvest/builtins/posix.d.ts +51 -0
- package/dist/command-harvest/builtins/posix.js +93 -0
- package/dist/command-harvest/harvest-commands.d.ts +66 -0
- package/dist/command-harvest/harvest-commands.js +97 -0
- package/dist/command-harvest/wrapper-table.d.ts +24 -0
- package/dist/command-harvest/wrapper-table.js +49 -0
- package/dist/configs/agent-authored.d.ts +41 -0
- package/dist/configs/agent-authored.js +33 -0
- package/dist/configs/bash-3-2.d.ts +28 -0
- package/dist/configs/bash-3-2.js +50 -0
- package/dist/configs/posix-portable.d.ts +40 -0
- package/dist/configs/posix-portable.js +52 -0
- package/dist/configs/recommended.d.ts +52 -0
- package/dist/configs/recommended.js +34 -0
- package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
- package/dist/declaration-conventions/comment-pragma.js +153 -0
- package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
- package/dist/declaration-conventions/declaration-convention.js +1 -0
- package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
- package/dist/declaration-conventions/read-declared-commands.js +128 -0
- package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
- package/dist/eslint-plugin-syntax.d.ts +27 -0
- package/dist/eslint-plugin.d.ts +1211 -52
- package/dist/index.d.ts +56 -8
- package/dist/index.js +53 -6
- package/dist/language/command-resolution.d.ts +124 -0
- package/dist/language/command-resolution.js +120 -0
- package/dist/language/end-of-input-marker.d.ts +29 -0
- package/dist/language/end-of-input-marker.js +88 -0
- package/dist/language/line-breaks.d.ts +18 -0
- package/dist/language/line-breaks.js +22 -0
- package/dist/language/lint-view-types.d.ts +104 -0
- package/dist/language/lint-view-types.js +1 -0
- package/dist/language/non-shell-file-guard.d.ts +35 -0
- package/dist/language/non-shell-file-guard.js +59 -0
- package/dist/language/selector-classes.d.ts +0 -1
- package/dist/language/selector-classes.js +0 -1
- package/dist/language/shell-language-options.d.ts +7 -1
- package/dist/language/shell-language-options.js +4 -4
- package/dist/language/shell-language.d.ts +10 -2
- package/dist/language/shell-language.js +59 -8
- package/dist/language/shell-source-code.d.ts +342 -5
- package/dist/language/shell-source-code.js +559 -30
- package/dist/language/source-coordinates.d.ts +13 -2
- package/dist/language/source-coordinates.js +68 -14
- package/dist/language/syntax-view.d.ts +60 -39
- package/dist/language/syntax-view.js +200 -45
- package/dist/language/word-resolution.d.ts +4 -3
- package/dist/language/word-resolution.js +4 -3
- package/dist/rules/compat/create-compat-rule.d.ts +59 -0
- package/dist/rules/compat/create-compat-rule.js +65 -0
- package/dist/rules/compat/declaration-options.d.ts +19 -0
- package/dist/rules/compat/declaration-options.js +110 -0
- package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
- package/dist/rules/compat/no-append-all-redirect.js +73 -0
- package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
- package/dist/rules/compat/no-associative-arrays.js +39 -0
- package/dist/rules/compat/no-case-attributes.d.ts +12 -0
- package/dist/rules/compat/no-case-attributes.js +63 -0
- package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
- package/dist/rules/compat/no-case-fallthrough.js +67 -0
- package/dist/rules/compat/no-case-modification.d.ts +35 -0
- package/dist/rules/compat/no-case-modification.js +86 -0
- package/dist/rules/compat/no-coproc.d.ts +12 -0
- package/dist/rules/compat/no-coproc.js +47 -0
- package/dist/rules/compat/no-declare-g.d.ts +12 -0
- package/dist/rules/compat/no-declare-g.js +59 -0
- package/dist/rules/compat/no-double-bracket.d.ts +12 -0
- package/dist/rules/compat/no-double-bracket.js +52 -0
- package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
- package/dist/rules/compat/no-epoch-variables.js +57 -0
- package/dist/rules/compat/no-fd-variable.d.ts +12 -0
- package/dist/rules/compat/no-fd-variable.js +48 -0
- package/dist/rules/compat/no-globstar.d.ts +12 -0
- package/dist/rules/compat/no-globstar.js +39 -0
- package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
- package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
- package/dist/rules/compat/no-let.d.ts +12 -0
- package/dist/rules/compat/no-let.js +62 -0
- package/dist/rules/compat/no-mapfile.d.ts +13 -0
- package/dist/rules/compat/no-mapfile.js +122 -0
- package/dist/rules/compat/no-nameref.d.ts +12 -0
- package/dist/rules/compat/no-nameref.js +44 -0
- package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
- package/dist/rules/compat/no-negative-array-index.js +123 -0
- package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
- package/dist/rules/compat/no-nounset-empty-array.js +158 -0
- package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
- package/dist/rules/compat/no-parameter-transformation.js +123 -0
- package/dist/rules/compat/no-pipe-all.d.ts +13 -0
- package/dist/rules/compat/no-pipe-all.js +68 -0
- package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
- package/dist/rules/compat/no-printf-time-format.js +99 -0
- package/dist/rules/compat/no-source.d.ts +13 -0
- package/dist/rules/compat/no-source.js +103 -0
- package/dist/rules/compat/no-test-v.d.ts +12 -0
- package/dist/rules/compat/no-test-v.js +280 -0
- package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
- package/dist/rules/compat/no-unicode-escape.js +120 -0
- package/dist/rules/compat/no-wait-n.d.ts +12 -0
- package/dist/rules/compat/no-wait-n.js +73 -0
- package/dist/rules/compat/operator-range.d.ts +10 -0
- package/dist/rules/compat/operator-range.js +15 -0
- package/dist/rules/compat/posix-absent-command.d.ts +12 -0
- package/dist/rules/compat/posix-absent-command.js +134 -0
- package/dist/rules/compat/shopt-options.d.ts +9 -0
- package/dist/rules/compat/shopt-options.js +63 -0
- package/dist/rules/compat/version-gates.d.ts +76 -0
- package/dist/rules/compat/version-gates.js +197 -0
- package/dist/rules/create-rule.d.ts +91 -0
- package/dist/rules/create-rule.js +51 -0
- package/dist/rules/create-wrapper-rule.d.ts +166 -0
- package/dist/rules/create-wrapper-rule.js +407 -0
- package/dist/rules/define-shell-rule.d.ts +184 -0
- package/dist/rules/define-shell-rule.js +56 -0
- package/dist/rules/index.d.ts +26 -0
- package/dist/rules/index.js +88 -0
- package/dist/rules/max-script-complexity.d.ts +28 -0
- package/dist/rules/max-script-complexity.js +130 -0
- package/dist/rules/no-dynamic-invocation.d.ts +35 -0
- package/dist/rules/no-dynamic-invocation.js +117 -0
- package/dist/rules/no-eval.d.ts +13 -0
- package/dist/rules/no-eval.js +89 -0
- package/dist/rules/no-masked-exit-status.d.ts +21 -0
- package/dist/rules/no-masked-exit-status.js +667 -0
- package/dist/rules/no-secret-env-reads.d.ts +24 -0
- package/dist/rules/no-secret-env-reads.js +133 -0
- package/dist/rules/no-secret-flags.d.ts +32 -0
- package/dist/rules/no-secret-flags.js +162 -0
- package/dist/rules/no-unchecked-cd.d.ts +44 -0
- package/dist/rules/no-unchecked-cd.js +250 -0
- package/dist/rules/no-undeclared-commands.d.ts +20 -0
- package/dist/rules/no-undeclared-commands.js +115 -0
- package/dist/rules/no-unused-declarations.d.ts +23 -0
- package/dist/rules/no-unused-declarations.js +259 -0
- package/dist/rules/prefer-local.d.ts +16 -0
- package/dist/rules/prefer-local.js +249 -0
- package/dist/rules/quote-expansions.d.ts +13 -0
- package/dist/rules/quote-expansions.js +229 -0
- package/dist/rules/require-network-wrapper.d.ts +40 -0
- package/dist/rules/require-network-wrapper.js +244 -0
- package/dist/rules/require-shebang.d.ts +29 -0
- package/dist/rules/require-shebang.js +101 -0
- package/dist/rules/require-strict-mode.d.ts +52 -0
- package/dist/rules/require-strict-mode.js +177 -0
- package/dist/rules/require-tty-guard.d.ts +13 -0
- package/dist/rules/require-tty-guard.js +289 -0
- package/dist/rules/resolve-declaration-convention.d.ts +96 -0
- package/dist/rules/resolve-declaration-convention.js +109 -0
- package/dist/rules/resolve-network-wrapper.d.ts +19 -0
- package/dist/rules/resolve-network-wrapper.js +116 -0
- package/dist/rules/set-options.d.ts +109 -0
- package/dist/rules/set-options.js +201 -0
- package/dist/rules/shebang-interpreter.d.ts +18 -0
- package/dist/rules/shebang-interpreter.js +81 -0
- package/dist/rules/shebang-matches-dialect.d.ts +12 -0
- package/dist/rules/shebang-matches-dialect.js +69 -0
- package/dist/rules/transparent-wrappers.d.ts +52 -0
- package/dist/rules/transparent-wrappers.js +99 -0
- package/dist/rules/user-pattern.d.ts +63 -0
- package/dist/rules/user-pattern.js +184 -0
- package/dist/syntax/analyze.d.ts +19 -0
- package/dist/syntax/analyze.js +21 -0
- package/dist/syntax/index.d.ts +23 -0
- package/dist/syntax/index.js +25 -0
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -1
- package/docs/compat.md +347 -0
- package/docs/custom-rule-tutorial.md +780 -34
- package/docs/declaration-conventions.md +478 -0
- package/docs/examples/audit-wrapper-pack.mjs +15 -0
- package/docs/examples/dep-line-convention.mjs +22 -0
- package/docs/examples/eslint.config.mjs +3 -2
- package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
- package/docs/examples/no-backticks-rule-tester.mjs +106 -0
- package/docs/examples/no-backticks-rule.mjs +60 -0
- package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
- package/docs/examples/no-function-keyword-rule.mjs +59 -0
- package/docs/examples/no-listed-commands-rule.ts +63 -0
- package/docs/examples/no-rm-rf-rule.mjs +26 -15
- package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
- package/docs/examples/sidecar-convention.mjs +38 -0
- package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
- package/docs/examples/sorted-declarations-rule.mjs +52 -0
- package/docs/reading-nodes.md +159 -0
- package/docs/rules/README.md +78 -0
- package/docs/rules/_template.md +83 -0
- package/docs/rules/compat/no-append-all-redirect.md +87 -0
- package/docs/rules/compat/no-associative-arrays.md +95 -0
- package/docs/rules/compat/no-case-attributes.md +82 -0
- package/docs/rules/compat/no-case-fallthrough.md +90 -0
- package/docs/rules/compat/no-case-modification.md +74 -0
- package/docs/rules/compat/no-coproc.md +76 -0
- package/docs/rules/compat/no-declare-g.md +80 -0
- package/docs/rules/compat/no-double-bracket.md +72 -0
- package/docs/rules/compat/no-epoch-variables.md +79 -0
- package/docs/rules/compat/no-fd-variable.md +78 -0
- package/docs/rules/compat/no-globstar.md +74 -0
- package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
- package/docs/rules/compat/no-let.md +78 -0
- package/docs/rules/compat/no-mapfile.md +118 -0
- package/docs/rules/compat/no-nameref.md +81 -0
- package/docs/rules/compat/no-negative-array-index.md +87 -0
- package/docs/rules/compat/no-nounset-empty-array.md +103 -0
- package/docs/rules/compat/no-parameter-transformation.md +94 -0
- package/docs/rules/compat/no-pipe-all.md +85 -0
- package/docs/rules/compat/no-printf-time-format.md +75 -0
- package/docs/rules/compat/no-source.md +98 -0
- package/docs/rules/compat/no-test-v.md +82 -0
- package/docs/rules/compat/no-unicode-escape.md +91 -0
- package/docs/rules/compat/no-wait-n.md +85 -0
- package/docs/rules/max-script-complexity.md +109 -0
- package/docs/rules/no-dynamic-invocation.md +139 -0
- package/docs/rules/no-eval.md +106 -0
- package/docs/rules/no-masked-exit-status.md +239 -0
- package/docs/rules/no-secret-env-reads.md +173 -0
- package/docs/rules/no-secret-flags.md +169 -0
- package/docs/rules/no-unchecked-cd.md +146 -0
- package/docs/rules/no-undeclared-commands.md +141 -0
- package/docs/rules/no-unused-declarations.md +216 -0
- package/docs/rules/prefer-local.md +221 -0
- package/docs/rules/quote-expansions.md +157 -0
- package/docs/rules/require-network-wrapper.md +171 -0
- package/docs/rules/require-shebang.md +157 -0
- package/docs/rules/require-strict-mode.md +253 -0
- package/docs/rules/require-tty-guard.md +128 -0
- package/docs/rules/shebang-matches-dialect.md +120 -0
- package/docs/selectors.md +2 -2
- package/docs/shellcheck.md +86 -0
- package/docs/wrapper-rules.md +206 -0
- package/package.json +26 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/language/selector-classes.d.ts.map +0 -1
- package/dist/language/selector-classes.js.map +0 -1
- package/dist/language/shell-language-options.d.ts.map +0 -1
- package/dist/language/shell-language-options.js.map +0 -1
- package/dist/language/shell-language.d.ts.map +0 -1
- package/dist/language/shell-language.js.map +0 -1
- package/dist/language/shell-source-code.d.ts.map +0 -1
- package/dist/language/shell-source-code.js.map +0 -1
- package/dist/language/source-coordinates.d.ts.map +0 -1
- package/dist/language/source-coordinates.js.map +0 -1
- package/dist/language/syntax-view.d.ts.map +0 -1
- package/dist/language/syntax-view.js.map +0 -1
- package/dist/language/word-resolution.d.ts.map +0 -1
- package/dist/language/word-resolution.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/quote-expansions`: reports an unquoted parameter expansion (`$var`,
|
|
3
|
+
* `${var}`, `$@`) or command substitution (`$(…)`, `` `…` ``) in a word the
|
|
4
|
+
* shell word-splits and pathname-expands.
|
|
5
|
+
*
|
|
6
|
+
* An unquoted expansion in such a word is split on `IFS` and each piece is
|
|
7
|
+
* glob-matched against the filesystem, so `rm $file` with
|
|
8
|
+
* `file='my notes.txt'` removes two files. Quoting the expansion
|
|
9
|
+
* (`rm "$file"`) keeps its result one word, which is almost always the intent.
|
|
10
|
+
*
|
|
11
|
+
* The rule decides from the expansion's position alone:
|
|
12
|
+
*
|
|
13
|
+
* - **Reported positions** (the word list contexts): command words and
|
|
14
|
+
* arguments of a simple command (including a declaration's bare operands
|
|
15
|
+
* such as `local $x`), the words of a `for`/`select … in` list, unkeyed
|
|
16
|
+
* array literal elements, and redirection targets other than here-documents
|
|
17
|
+
* (here-strings included, since bash before 4.4 split them).
|
|
18
|
+
* - **Exempt positions**, by construction: every other place a word can occur
|
|
19
|
+
* (`[[ ]]` operands, assignment values and keyed array elements
|
|
20
|
+
* (`arr=([k]=$v)`), `case` subjects and patterns,
|
|
21
|
+
* arithmetic, here-document delimiters and bodies), an expansion that is a
|
|
22
|
+
* part inside double quotes, and an expansion nested inside another
|
|
23
|
+
* parameter expansion (`${a:-$b}`, judged as the outer one).
|
|
24
|
+
* - **Exempt expansions**: the specials that always expand to one word with no
|
|
25
|
+
* glob characters (`$#`, `$?`, `$$`, `$!`, `$-`), length expansions
|
|
26
|
+
* (`${#var}`), and, under the `bash` and `bats` dialects, the Bash
|
|
27
|
+
* variables that always hold an integer (`$RANDOM`, `$SECONDS`, `$LINENO`,
|
|
28
|
+
* `$PPID`, `$OPTIND`, `$BASH_SUBSHELL`).
|
|
29
|
+
*
|
|
30
|
+
* - **zsh**: under `dialect: "zsh"`, parameter expansions are not reported,
|
|
31
|
+
* because zsh neither splits nor globs an unquoted parameter by default.
|
|
32
|
+
* Command substitutions still report there, since zsh does split them. A
|
|
33
|
+
* zsh script that enables `SH_WORD_SPLIT` or `GLOB_SUBST` is not detected.
|
|
34
|
+
*
|
|
35
|
+
* Only the syntactic position counts: the rule does no dataflow, so
|
|
36
|
+
* `n=3; seq $n` reports even though `$n` cannot split. Each report carries a
|
|
37
|
+
* suggestion that wraps the one expansion in double quotes; it is never an
|
|
38
|
+
* autofix, because a script may rely on the splitting (for example
|
|
39
|
+
* `$CFLAGS` holding several flags), and only its author can say so.
|
|
40
|
+
*/
|
|
41
|
+
import { DashHdoc, Hdoc } from 'sh-ast/analyze';
|
|
42
|
+
import { createRule } from './create-rule.js';
|
|
43
|
+
import { messageExcerpt } from '../language/line-breaks.js';
|
|
44
|
+
/** The registry name of this rule (`sh/quote-expansions` in a user's config). */
|
|
45
|
+
const RULE_NAME = 'quote-expansions';
|
|
46
|
+
/**
|
|
47
|
+
* Special parameters whose expansion is always a single word with no glob
|
|
48
|
+
* characters: the argument count, exit status, shell and background PIDs,
|
|
49
|
+
* and the option-flag letters. Positional and named parameters are not here:
|
|
50
|
+
* their values are arbitrary text.
|
|
51
|
+
*/
|
|
52
|
+
const SINGLE_WORD_SPECIALS = new Set(['#', '?', '$', '!', '-']);
|
|
53
|
+
/**
|
|
54
|
+
* Bash variables that always expand to a decimal integer, so an unquoted
|
|
55
|
+
* reference is one word with no glob characters, like the specials above.
|
|
56
|
+
* Bash keeps them integers: assigning text to `RANDOM`, `SECONDS` or `OPTIND`
|
|
57
|
+
* evaluates it arithmetically, `LINENO` and `BASH_SUBSHELL` ignore
|
|
58
|
+
* assignments, and `PPID` is read-only; a hostile value in the environment is
|
|
59
|
+
* ignored for each of them. Each exists in every Bash from 3.2, the oldest
|
|
60
|
+
* target eslint-sh supports.
|
|
61
|
+
*
|
|
62
|
+
* Left out on purpose: `UID` and `EUID`, which Bash imports verbatim from the
|
|
63
|
+
* environment (`env UID='a *' bash -c 'echo "[$UID]"'` prints `[a *]`), so
|
|
64
|
+
* they can hold text that splits and globs; `BASHPID` (Bash 4.0), `EPOCHSECONDS` (5.0) and
|
|
65
|
+
* `SRANDOM` (5.1), which an older Bash leaves unset, so an unquoted reference
|
|
66
|
+
* there expands to no word at all; `EPOCHREALTIME`, whose decimal separator
|
|
67
|
+
* follows the locale; and variables a script or the environment may set to
|
|
68
|
+
* any text (`COLUMNS`, `SHLVL`). The list does not apply outside Bash: in a
|
|
69
|
+
* POSIX shell such as dash these are ordinary variables.
|
|
70
|
+
*/
|
|
71
|
+
const BASH_INTEGER_VARIABLES = new Set([
|
|
72
|
+
'RANDOM',
|
|
73
|
+
'SECONDS',
|
|
74
|
+
'LINENO',
|
|
75
|
+
'PPID',
|
|
76
|
+
'OPTIND',
|
|
77
|
+
'BASH_SUBSHELL',
|
|
78
|
+
]);
|
|
79
|
+
/** The dialects whose scripts run under Bash, where {@link BASH_INTEGER_VARIABLES} holds. */
|
|
80
|
+
const BASH_DIALECTS = new Set(['bash', 'bats']);
|
|
81
|
+
/** Redirection operators that introduce a here-document (`<<`, `<<-`). */
|
|
82
|
+
const HEREDOC_OPERATORS = new Set([Hdoc, DashHdoc]);
|
|
83
|
+
/**
|
|
84
|
+
* Whether the parameter expansion `node` always yields one word: a length
|
|
85
|
+
* expansion (`${#var}`, `${#arr[@]}`), or a bare reference with no operator
|
|
86
|
+
* that could substitute other text to a single-word special (`$?`, `${#}`)
|
|
87
|
+
* or, when `bashIntegers` is set, to a Bash integer variable (`$RANDOM`).
|
|
88
|
+
* Indirection disqualifies a reference: `${!#}` expands the last positional
|
|
89
|
+
* parameter, whose value is arbitrary text, not the argument count.
|
|
90
|
+
*/
|
|
91
|
+
function isSingleWordParamExp(node, bashIntegers) {
|
|
92
|
+
if (node.length) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const name = node.param?.value;
|
|
96
|
+
return (name !== undefined &&
|
|
97
|
+
(SINGLE_WORD_SPECIALS.has(name) || (bashIntegers && BASH_INTEGER_VARIABLES.has(name))) &&
|
|
98
|
+
!node.excl &&
|
|
99
|
+
node.exp === undefined &&
|
|
100
|
+
node.repl === undefined &&
|
|
101
|
+
node.slice === undefined &&
|
|
102
|
+
node.index === undefined);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The declaration utilities whose `NAME=value` operands are assignments. Under
|
|
106
|
+
* `dialect: "bash"` the parser gives them a declaration node with `Assign`
|
|
107
|
+
* children; under `dialect: "posix"` it leaves them as ordinary commands, so
|
|
108
|
+
* the same operand arrives as a plain argument word and is recognized by
|
|
109
|
+
* {@link isAssignmentOperand} instead.
|
|
110
|
+
*/
|
|
111
|
+
const DECLARATION_UTILITIES = new Set([
|
|
112
|
+
'export',
|
|
113
|
+
'readonly',
|
|
114
|
+
'local',
|
|
115
|
+
'declare',
|
|
116
|
+
'typeset',
|
|
117
|
+
]);
|
|
118
|
+
/** The literal start of an assignment operand: a variable name and `=`. */
|
|
119
|
+
const ASSIGNMENT_PREFIX = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
|
120
|
+
/**
|
|
121
|
+
* Whether `word`, an argument of the plain command `callExpr`, is the
|
|
122
|
+
* `NAME=value` operand of a declaration utility written directly as the
|
|
123
|
+
* command (`local x=$y` parsed as a plain command). Its value is assigned, not
|
|
124
|
+
* split, exactly as in the declaration node the bash dialect produces; a bare
|
|
125
|
+
* operand (`local $names`) is not an assignment and still reports.
|
|
126
|
+
*/
|
|
127
|
+
function isAssignmentOperand(word, callExpr) {
|
|
128
|
+
const { args } = callExpr;
|
|
129
|
+
if (word.type !== 'Word' || args.indexOf(word) < 1) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const commandParts = args[0].parts;
|
|
133
|
+
const command = commandParts[0];
|
|
134
|
+
const first = word.parts[0];
|
|
135
|
+
return (commandParts.length === 1 &&
|
|
136
|
+
command.type === 'Lit' &&
|
|
137
|
+
DECLARATION_UTILITIES.has(command.value) &&
|
|
138
|
+
first.type === 'Lit' &&
|
|
139
|
+
ASSIGNMENT_PREFIX.test(first.value));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Whether `word`, a `Word` node whose parent is `parent`, is in a position the
|
|
143
|
+
* shell word-splits and pathname-expands. This is the complete list of such
|
|
144
|
+
* positions; anything not named here is exempt.
|
|
145
|
+
*/
|
|
146
|
+
function isSplitWord(word, parent) {
|
|
147
|
+
switch (parent?.type) {
|
|
148
|
+
// Command words and arguments, except a declaration utility's assignment
|
|
149
|
+
// operand; also each element of a `for`/`select` list.
|
|
150
|
+
case 'CallExpr':
|
|
151
|
+
return !isAssignmentOperand(word, parent);
|
|
152
|
+
case 'WordIter':
|
|
153
|
+
return true;
|
|
154
|
+
// A plain array literal element (`arr=($x)`). A keyed element
|
|
155
|
+
// (`arr=([k]=$x)`) is assigned like a variable, without splitting.
|
|
156
|
+
case 'ArrayElem':
|
|
157
|
+
return parent.index === undefined && parent.value === word;
|
|
158
|
+
// A bare declaration operand (`local $x`, `export $names`) is an ordinary
|
|
159
|
+
// argument to the builtin; an assignment value (`local x=$y`) is not split.
|
|
160
|
+
case 'Assign':
|
|
161
|
+
return parent.naked && parent.value === word;
|
|
162
|
+
// A redirection target, except a here-document's delimiter and body.
|
|
163
|
+
case 'Redirect':
|
|
164
|
+
return !HEREDOC_OPERATORS.has(parent.op) && parent.word === word;
|
|
165
|
+
default:
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The `sh/quote-expansions` rule module.
|
|
171
|
+
*
|
|
172
|
+
* @internal
|
|
173
|
+
*/
|
|
174
|
+
export const quoteExpansions = createRule({
|
|
175
|
+
name: RULE_NAME,
|
|
176
|
+
meta: {
|
|
177
|
+
type: 'suggestion',
|
|
178
|
+
docs: {
|
|
179
|
+
description: 'Require double quotes around parameter expansions and command substitutions in words the shell splits and globs',
|
|
180
|
+
},
|
|
181
|
+
hasSuggestions: true,
|
|
182
|
+
schema: [],
|
|
183
|
+
messages: {
|
|
184
|
+
unquotedExpansion: 'Unquoted `{{expansion}}` undergoes field splitting on `IFS` and pathname expansion. Quote it unless the splitting is intended.',
|
|
185
|
+
wrapInQuotes: 'Wrap `{{expansion}}` in double quotes.',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
create(context) {
|
|
189
|
+
const { sourceCode } = context;
|
|
190
|
+
// zsh splits command substitutions but not parameters (absent
|
|
191
|
+
// `SH_WORD_SPLIT`/`GLOB_SUBST`), so only `CmdSubst` is checked there.
|
|
192
|
+
const checkParamExp = sourceCode.dialect !== 'zsh';
|
|
193
|
+
const bashIntegers = BASH_DIALECTS.has(sourceCode.dialect);
|
|
194
|
+
/**
|
|
195
|
+
* Reports `node` (a `ParamExp` or `CmdSubst`) when it is a direct,
|
|
196
|
+
* unquoted part of a word in a split position. A part inside `"…"` has a
|
|
197
|
+
* `DblQuoted` parent instead of the `Word`, so it never matches here.
|
|
198
|
+
*/
|
|
199
|
+
function check(node) {
|
|
200
|
+
const word = sourceCode.getParent(node);
|
|
201
|
+
if (word?.type !== 'Word' || !isSplitWord(word, sourceCode.getParent(word))) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (node.type === 'ParamExp' && isSingleWordParamExp(node, bashIntegers)) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const expansion = sourceCode.text.slice(node.range[0], node.range[1]);
|
|
208
|
+
// Inside double quotes, POSIX leaves a backquoted substitution containing
|
|
209
|
+
// `"` undefined, and `\` changes meaning; such a substitution is reported
|
|
210
|
+
// without a suggestion rather than with one that may change behavior.
|
|
211
|
+
const quotable = !(node.type === 'CmdSubst' && node.backquotes && /["\\]/.test(expansion));
|
|
212
|
+
context.report({
|
|
213
|
+
node,
|
|
214
|
+
messageId: 'unquotedExpansion',
|
|
215
|
+
data: { expansion: messageExcerpt(expansion) },
|
|
216
|
+
suggest: quotable
|
|
217
|
+
? [
|
|
218
|
+
{
|
|
219
|
+
messageId: 'wrapInQuotes',
|
|
220
|
+
data: { expansion: messageExcerpt(expansion) },
|
|
221
|
+
fix: (fixer) => fixer.replaceText(node, `"${expansion}"`),
|
|
222
|
+
},
|
|
223
|
+
]
|
|
224
|
+
: [],
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return checkParamExp ? { ParamExp: check, CmdSubst: check } : { CmdSubst: check };
|
|
228
|
+
},
|
|
229
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/require-network-wrapper`: every network command must be run through the
|
|
3
|
+
* project's configured wrapper (a retry, audit, or proxy helper).
|
|
4
|
+
*
|
|
5
|
+
* This is the built-in instance of `createWrapperRule`: the commands
|
|
6
|
+
* default to common network clients, and the wrapper comes from the user's
|
|
7
|
+
* rule's `wrapper` option, or else from the shared `settings.sh.networkWrapper`
|
|
8
|
+
* (which the declaration rules also read). The wrapper has **no default**.
|
|
9
|
+
* Which wrapper guards network calls is a project's policy, and a generic
|
|
10
|
+
* linter must not invent it, so with neither set the rule is inert: it
|
|
11
|
+
* registers no listeners, reports nothing, and never analyzes the file.
|
|
12
|
+
*
|
|
13
|
+
* `git` counts only for the subcommands that talk to a remote (`fetch`,
|
|
14
|
+
* `clone`, `pull`, `push`), found after git's own global options
|
|
15
|
+
* (`git -C dir fetch`).
|
|
16
|
+
*/
|
|
17
|
+
import type { WrapperSpec } from 'sh-ast/analyze';
|
|
18
|
+
/**
|
|
19
|
+
* The options of `sh/require-network-wrapper`.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export interface RequireNetworkWrapperOptions {
|
|
24
|
+
/** Replaces the default network commands. */
|
|
25
|
+
readonly commands?: readonly string[];
|
|
26
|
+
/**
|
|
27
|
+
* The wrapper network commands must be run through: a name, or a
|
|
28
|
+
* `WrapperSpec` declaring the wrapper's option shape. Unset falls back to
|
|
29
|
+
* `settings.sh.networkWrapper`; with neither, the rule is inert.
|
|
30
|
+
*/
|
|
31
|
+
readonly wrapper?: string | WrapperSpec;
|
|
32
|
+
/** Commands checked in addition to `commands` (or its default). */
|
|
33
|
+
readonly alsoTreatAsNetwork?: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The `sh/require-network-wrapper` rule module.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export declare const requireNetworkWrapper: import("./define-shell-rule.js").ShellRule<[RequireNetworkWrapperOptions?], "unwrappedCommand">;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { createRule } from './create-rule.js';
|
|
2
|
+
import { WRAPPER_RULE_MESSAGES, buildWrapperRule } from './create-wrapper-rule.js';
|
|
3
|
+
import { readNetworkWrapperSetting } from './resolve-network-wrapper.js';
|
|
4
|
+
/** The registry name of this rule (`sh/require-network-wrapper` in a user's config). */
|
|
5
|
+
const RULE_NAME = 'require-network-wrapper';
|
|
6
|
+
/**
|
|
7
|
+
* The commands checked when the `commands` option is not set: clients whose
|
|
8
|
+
* normal job is to talk to another host.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_NETWORK_COMMANDS = [
|
|
11
|
+
'curl',
|
|
12
|
+
'wget',
|
|
13
|
+
'nc',
|
|
14
|
+
'ssh',
|
|
15
|
+
'scp',
|
|
16
|
+
'rsync',
|
|
17
|
+
'git',
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* `git`'s network subcommands, and the global options that may precede a
|
|
21
|
+
* subcommand, so that `git -C dir fetch` is found as `fetch`.
|
|
22
|
+
*
|
|
23
|
+
* The option lists follow git(1)'s synopsis and OPTIONS. Options git treats
|
|
24
|
+
* as "print something and exit" (`--version`, `--help`, the `--*-path`
|
|
25
|
+
* queries, and `--exec-path` without a value) run no subcommand. An option not
|
|
26
|
+
* listed here leaves the subcommand unknown, so the invocation is not
|
|
27
|
+
* reported rather than guessed.
|
|
28
|
+
*
|
|
29
|
+
* @see https://git-scm.com/docs/git#_options
|
|
30
|
+
*/
|
|
31
|
+
const GIT_NETWORK_SUBCOMMANDS = {
|
|
32
|
+
names: ['fetch', 'clone', 'pull', 'push'],
|
|
33
|
+
noArgFlags: [
|
|
34
|
+
'-p',
|
|
35
|
+
'--paginate',
|
|
36
|
+
'-P',
|
|
37
|
+
'--no-pager',
|
|
38
|
+
'--no-replace-objects',
|
|
39
|
+
'--no-lazy-fetch',
|
|
40
|
+
'--no-optional-locks',
|
|
41
|
+
'--no-advice',
|
|
42
|
+
'--bare',
|
|
43
|
+
'--literal-pathspecs',
|
|
44
|
+
'--glob-pathspecs',
|
|
45
|
+
'--noglob-pathspecs',
|
|
46
|
+
'--icase-pathspecs',
|
|
47
|
+
],
|
|
48
|
+
argFlags: [
|
|
49
|
+
'-C',
|
|
50
|
+
'-c',
|
|
51
|
+
'--git-dir',
|
|
52
|
+
'--work-tree',
|
|
53
|
+
'--namespace',
|
|
54
|
+
'--config-env',
|
|
55
|
+
'--attr-source',
|
|
56
|
+
// Only `--exec-path=<path>` reaches here: the bare flag is in
|
|
57
|
+
// `stopsChainFlags`, which is checked first.
|
|
58
|
+
'--exec-path',
|
|
59
|
+
],
|
|
60
|
+
stopsChainFlags: [
|
|
61
|
+
'-v',
|
|
62
|
+
'--version',
|
|
63
|
+
'-h',
|
|
64
|
+
'--help',
|
|
65
|
+
'--html-path',
|
|
66
|
+
'--man-path',
|
|
67
|
+
'--info-path',
|
|
68
|
+
'--exec-path',
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* The subcommand restrictions this rule always applies: `git` counts only for
|
|
73
|
+
* its network subcommands, whether it comes from the default `commands`, a
|
|
74
|
+
* configured `commands`, or `alsoTreatAsNetwork`.
|
|
75
|
+
*/
|
|
76
|
+
const NETWORK_SUBCOMMANDS = {
|
|
77
|
+
git: GIT_NETWORK_SUBCOMMANDS,
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* The invocations of the default network commands that touch no network:
|
|
81
|
+
* their version and help output, and `git clone` from a local path. These
|
|
82
|
+
* are the rule's own binding of the factory's `localInvocations`; the factory
|
|
83
|
+
* names no command.
|
|
84
|
+
*
|
|
85
|
+
* `curl --help all` is curl's full help listing, and `-V` and `-h` are the
|
|
86
|
+
* short forms of `--version` and `--help`. The argument lists match exactly,
|
|
87
|
+
* so `curl --version https://example.com`, which still makes the request, is
|
|
88
|
+
* reported. A clone source is local when it is a path (`./x`, `../x`, `/x`) or
|
|
89
|
+
* a `file://` URL; a dynamic source or a remote URL is not, and only the
|
|
90
|
+
* source decides, so a dynamic destination does not matter. The `git clone`
|
|
91
|
+
* option lists follow git-clone(1), minus the options that can reach a
|
|
92
|
+
* remote whatever the source is: `--recurse-submodules`, `--recursive` and
|
|
93
|
+
* `--remote-submodules` (a local repository's `.gitmodules` may point at a
|
|
94
|
+
* remote URL) and `--bundle-uri` (which fetches the bundle from its URL). Such
|
|
95
|
+
* a clone is not provably local and is reported. `--no-recurse-submodules`
|
|
96
|
+
* only turns recursion off, so it is skipped.
|
|
97
|
+
*
|
|
98
|
+
* @see https://git-scm.com/docs/git-clone
|
|
99
|
+
*/
|
|
100
|
+
const NETWORK_LOCAL_INVOCATIONS = {
|
|
101
|
+
curl: {
|
|
102
|
+
argLists: [['--version'], ['-V'], ['--help'], ['-h'], ['--help', 'all'], ['-h', 'all']],
|
|
103
|
+
},
|
|
104
|
+
wget: { argLists: [['--version'], ['-V'], ['--help'], ['-h']] },
|
|
105
|
+
'git clone': {
|
|
106
|
+
localOperand: {
|
|
107
|
+
localPrefixes: ['./', '../', '/', 'file://'],
|
|
108
|
+
noArgFlags: [
|
|
109
|
+
'-l',
|
|
110
|
+
'--local',
|
|
111
|
+
'--no-hardlinks',
|
|
112
|
+
'-s',
|
|
113
|
+
'--shared',
|
|
114
|
+
'--dissociate',
|
|
115
|
+
'-q',
|
|
116
|
+
'--quiet',
|
|
117
|
+
'-v',
|
|
118
|
+
'--verbose',
|
|
119
|
+
'--progress',
|
|
120
|
+
'-n',
|
|
121
|
+
'--no-checkout',
|
|
122
|
+
'--bare',
|
|
123
|
+
'--mirror',
|
|
124
|
+
'--single-branch',
|
|
125
|
+
'--no-single-branch',
|
|
126
|
+
'--no-tags',
|
|
127
|
+
'--shallow-submodules',
|
|
128
|
+
'--no-shallow-submodules',
|
|
129
|
+
'--sparse',
|
|
130
|
+
'--reject-shallow',
|
|
131
|
+
'--no-reject-shallow',
|
|
132
|
+
'--no-recurse-submodules',
|
|
133
|
+
'--also-filter-submodules',
|
|
134
|
+
'-4',
|
|
135
|
+
'--ipv4',
|
|
136
|
+
'-6',
|
|
137
|
+
'--ipv6',
|
|
138
|
+
],
|
|
139
|
+
argFlags: [
|
|
140
|
+
'-o',
|
|
141
|
+
'--origin',
|
|
142
|
+
'-b',
|
|
143
|
+
'--branch',
|
|
144
|
+
'-u',
|
|
145
|
+
'--upload-pack',
|
|
146
|
+
'--reference',
|
|
147
|
+
'--reference-if-able',
|
|
148
|
+
'--separate-git-dir',
|
|
149
|
+
'--depth',
|
|
150
|
+
'--shallow-since',
|
|
151
|
+
'--shallow-exclude',
|
|
152
|
+
'-c',
|
|
153
|
+
'--config',
|
|
154
|
+
'--template',
|
|
155
|
+
'--filter',
|
|
156
|
+
'-j',
|
|
157
|
+
'--jobs',
|
|
158
|
+
'--server-option',
|
|
159
|
+
'--ref-format',
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
/** A JSON-schema array of non-empty strings. */
|
|
165
|
+
const STRING_LIST_SCHEMA = {
|
|
166
|
+
type: 'array',
|
|
167
|
+
items: { type: 'string', minLength: 1 },
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* The JSON-expressible form of a `WrapperSpec`, for the `wrapper` option.
|
|
171
|
+
* `noArgFlagPattern` (a `RegExp`) and the legacy `skipAssignmentOperands` are
|
|
172
|
+
* left out; a rule pack that needs them binds its wrapper with
|
|
173
|
+
* `createWrapperRule` instead.
|
|
174
|
+
*/
|
|
175
|
+
const WRAPPER_SPEC_SCHEMA = {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: {
|
|
178
|
+
names: { ...STRING_LIST_SCHEMA, minItems: 1 },
|
|
179
|
+
noArgFlags: STRING_LIST_SCHEMA,
|
|
180
|
+
argFlags: STRING_LIST_SCHEMA,
|
|
181
|
+
unresolvableFlags: STRING_LIST_SCHEMA,
|
|
182
|
+
stopsChainFlags: STRING_LIST_SCHEMA,
|
|
183
|
+
positionalOperandsBeforeCommand: { type: 'integer', minimum: 0 },
|
|
184
|
+
assignmentOperands: { enum: ['shell-identifier', 'env', 'sudo'] },
|
|
185
|
+
},
|
|
186
|
+
required: ['names'],
|
|
187
|
+
additionalProperties: false,
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Resolves the factory config this rule enforces for one set of options, or
|
|
191
|
+
* `null` when no wrapper is configured.
|
|
192
|
+
*/
|
|
193
|
+
function wrapperRuleConfig(options, settings) {
|
|
194
|
+
// The rule option wins; otherwise the shared setting, which the
|
|
195
|
+
// declaration rules read too (see `./resolve-network-wrapper.ts`).
|
|
196
|
+
const wrapper = options?.wrapper ?? readNetworkWrapperSetting(settings);
|
|
197
|
+
if (wrapper === undefined)
|
|
198
|
+
return null;
|
|
199
|
+
return {
|
|
200
|
+
commands: [
|
|
201
|
+
...(options?.commands ?? DEFAULT_NETWORK_COMMANDS),
|
|
202
|
+
...(options?.alsoTreatAsNetwork ?? []),
|
|
203
|
+
],
|
|
204
|
+
wrapper,
|
|
205
|
+
subcommands: NETWORK_SUBCOMMANDS,
|
|
206
|
+
localInvocations: NETWORK_LOCAL_INVOCATIONS,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The `sh/require-network-wrapper` rule module.
|
|
211
|
+
*
|
|
212
|
+
* @internal
|
|
213
|
+
*/
|
|
214
|
+
export const requireNetworkWrapper = createRule({
|
|
215
|
+
name: RULE_NAME,
|
|
216
|
+
meta: {
|
|
217
|
+
type: 'problem',
|
|
218
|
+
docs: {
|
|
219
|
+
description: 'Require network commands to be run through the configured wrapper',
|
|
220
|
+
},
|
|
221
|
+
schema: [
|
|
222
|
+
{
|
|
223
|
+
type: 'object',
|
|
224
|
+
properties: {
|
|
225
|
+
commands: STRING_LIST_SCHEMA,
|
|
226
|
+
wrapper: { oneOf: [{ type: 'string', minLength: 1 }, WRAPPER_SPEC_SCHEMA] },
|
|
227
|
+
alsoTreatAsNetwork: STRING_LIST_SCHEMA,
|
|
228
|
+
},
|
|
229
|
+
additionalProperties: false,
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
messages: WRAPPER_RULE_MESSAGES,
|
|
233
|
+
},
|
|
234
|
+
create(context) {
|
|
235
|
+
const config = wrapperRuleConfig(context.options[0], context.settings);
|
|
236
|
+
if (config === null)
|
|
237
|
+
return {};
|
|
238
|
+
// The check itself is the rule `createWrapperRule` makes for this config
|
|
239
|
+
// (`buildWrapperRule` is that factory, typed for built-in rules); this
|
|
240
|
+
// rule only derives the config from the user's options and reports
|
|
241
|
+
// through its own context.
|
|
242
|
+
return buildWrapperRule(config).create(context);
|
|
243
|
+
},
|
|
244
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the fix spells the interpreter it inserts: `env` resolves the shell
|
|
3
|
+
* through `PATH` (`#!/usr/bin/env bash`), `absolute` names a fixed location
|
|
4
|
+
* (`#!/bin/bash`).
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export type ShebangStyle = 'env' | 'absolute';
|
|
9
|
+
/**
|
|
10
|
+
* The rule's single options object, as written in a config.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export interface RequireShebangOptions {
|
|
15
|
+
readonly style?: ShebangStyle;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The rule's report kinds: no shebang on line 1, or a shebang that does not run
|
|
19
|
+
* a recognized shell.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export type RequireShebangMessageIds = 'missingShebang' | 'unrecognizedInterpreter';
|
|
24
|
+
/**
|
|
25
|
+
* The rule module registered as `require-shebang`.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export declare const requireShebang: import("./define-shell-rule.js").ShellRule<[RequireShebangOptions], RequireShebangMessageIds>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createRule } from './create-rule.js';
|
|
2
|
+
import { shebangShell } from './shebang-interpreter.js';
|
|
3
|
+
/**
|
|
4
|
+
* Interpreter basenames this rule accepts in a shebang. Any of them makes the
|
|
5
|
+
* file's interpreter explicit; which one is right for the configured dialect
|
|
6
|
+
* is a separate rule's question.
|
|
7
|
+
*/
|
|
8
|
+
const RECOGNIZED_SHELLS = new Set([
|
|
9
|
+
'bash',
|
|
10
|
+
'sh',
|
|
11
|
+
'dash',
|
|
12
|
+
'ksh',
|
|
13
|
+
'mksh',
|
|
14
|
+
'zsh',
|
|
15
|
+
'bats',
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* The interpreter the autofix names for each configured dialect. `posix` has
|
|
19
|
+
* no shell of its own name, so it maps to `sh`.
|
|
20
|
+
*/
|
|
21
|
+
const SHELL_FOR_DIALECT = {
|
|
22
|
+
bash: 'bash',
|
|
23
|
+
posix: 'sh',
|
|
24
|
+
mksh: 'mksh',
|
|
25
|
+
zsh: 'zsh',
|
|
26
|
+
bats: 'bats',
|
|
27
|
+
};
|
|
28
|
+
/** The UTF-8 byte order mark as it appears in a decoded string. */
|
|
29
|
+
const BOM = '\uFEFF';
|
|
30
|
+
/** The shebang line the fix inserts for `dialect`, in the configured `style`. */
|
|
31
|
+
function shebangFor(dialect, style) {
|
|
32
|
+
const shell = SHELL_FOR_DIALECT[dialect];
|
|
33
|
+
return style === 'env' ? `#!/usr/bin/env ${shell}` : `#!/bin/${shell}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The rule module registered as `require-shebang`.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export const requireShebang = createRule({
|
|
41
|
+
name: 'require-shebang',
|
|
42
|
+
meta: {
|
|
43
|
+
type: 'problem',
|
|
44
|
+
docs: {
|
|
45
|
+
description: 'Require a shebang on line 1 that runs a recognized shell',
|
|
46
|
+
},
|
|
47
|
+
fixable: 'code',
|
|
48
|
+
schema: [
|
|
49
|
+
{
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
style: { enum: ['env', 'absolute'] },
|
|
53
|
+
},
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
defaultOptions: [{ style: 'env' }],
|
|
58
|
+
messages: {
|
|
59
|
+
missingShebang: "This file has no shebang, so it runs under whichever shell invokes it. Add one on line 1, such as '{{shebang}}'.",
|
|
60
|
+
unrecognizedInterpreter: "The shebang '{{shebang}}' does not run a recognized shell. Name bash, sh, dash, ksh, mksh, zsh or bats by absolute path, or through env (such as /usr/bin/env).",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
create(context) {
|
|
64
|
+
const { sourceCode } = context;
|
|
65
|
+
// `defaultOptions` guarantees the options object; `style` may still be omitted in it.
|
|
66
|
+
const [{ style = 'env' }] = context.options;
|
|
67
|
+
return {
|
|
68
|
+
File() {
|
|
69
|
+
const text = sourceCode.text.startsWith(BOM) ? sourceCode.text.slice(1) : sourceCode.text;
|
|
70
|
+
const firstLine = sourceCode.lines[0] ?? '';
|
|
71
|
+
// Line 1 in full. The shell language numbers lines and columns from 1
|
|
72
|
+
// (`ShellLanguage.lineStart`/`columnStart`).
|
|
73
|
+
const loc = {
|
|
74
|
+
start: { line: 1, column: 1 },
|
|
75
|
+
end: { line: 1, column: firstLine.length + 1 },
|
|
76
|
+
};
|
|
77
|
+
if (text.startsWith('#!')) {
|
|
78
|
+
const shebangLine = firstLine.startsWith(BOM) ? firstLine.slice(1) : firstLine;
|
|
79
|
+
const shell = shebangShell(shebangLine.slice('#!'.length));
|
|
80
|
+
if (shell === undefined || !RECOGNIZED_SHELLS.has(shell)) {
|
|
81
|
+
context.report({
|
|
82
|
+
loc,
|
|
83
|
+
messageId: 'unrecognizedInterpreter',
|
|
84
|
+
data: { shebang: shebangLine.trimEnd() },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const shebang = shebangFor(sourceCode.dialect, style);
|
|
90
|
+
// Keep the file's own line-ending style: a CRLF file gets a CRLF shebang.
|
|
91
|
+
const lineBreak = /^[^\n]*\r\n/u.test(sourceCode.text) ? '\r\n' : '\n';
|
|
92
|
+
context.report({
|
|
93
|
+
loc,
|
|
94
|
+
messageId: 'missingShebang',
|
|
95
|
+
data: { shebang },
|
|
96
|
+
fix: (fixer) => fixer.insertTextBeforeRange([0, 0], `${shebang}${lineBreak}`),
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
});
|