@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.
Files changed (256) hide show
  1. package/README.md +223 -17
  2. package/dist/command-harvest/builtins/bash.d.ts +36 -0
  3. package/dist/command-harvest/builtins/bash.js +98 -0
  4. package/dist/command-harvest/builtins/bats.d.ts +45 -0
  5. package/dist/command-harvest/builtins/bats.js +33 -0
  6. package/dist/command-harvest/builtins/index.d.ts +18 -0
  7. package/dist/command-harvest/builtins/index.js +39 -0
  8. package/dist/command-harvest/builtins/posix.d.ts +51 -0
  9. package/dist/command-harvest/builtins/posix.js +93 -0
  10. package/dist/command-harvest/harvest-commands.d.ts +66 -0
  11. package/dist/command-harvest/harvest-commands.js +97 -0
  12. package/dist/command-harvest/wrapper-table.d.ts +24 -0
  13. package/dist/command-harvest/wrapper-table.js +49 -0
  14. package/dist/configs/agent-authored.d.ts +41 -0
  15. package/dist/configs/agent-authored.js +33 -0
  16. package/dist/configs/bash-3-2.d.ts +28 -0
  17. package/dist/configs/bash-3-2.js +50 -0
  18. package/dist/configs/posix-portable.d.ts +40 -0
  19. package/dist/configs/posix-portable.js +52 -0
  20. package/dist/configs/recommended.d.ts +52 -0
  21. package/dist/configs/recommended.js +34 -0
  22. package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
  23. package/dist/declaration-conventions/comment-pragma.js +153 -0
  24. package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
  25. package/dist/declaration-conventions/declaration-convention.js +1 -0
  26. package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
  27. package/dist/declaration-conventions/read-declared-commands.js +128 -0
  28. package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
  29. package/dist/eslint-plugin-syntax.d.ts +27 -0
  30. package/dist/eslint-plugin.d.ts +1211 -52
  31. package/dist/index.d.ts +56 -8
  32. package/dist/index.js +53 -6
  33. package/dist/language/command-resolution.d.ts +124 -0
  34. package/dist/language/command-resolution.js +120 -0
  35. package/dist/language/end-of-input-marker.d.ts +29 -0
  36. package/dist/language/end-of-input-marker.js +88 -0
  37. package/dist/language/line-breaks.d.ts +18 -0
  38. package/dist/language/line-breaks.js +22 -0
  39. package/dist/language/lint-view-types.d.ts +104 -0
  40. package/dist/language/lint-view-types.js +1 -0
  41. package/dist/language/non-shell-file-guard.d.ts +35 -0
  42. package/dist/language/non-shell-file-guard.js +59 -0
  43. package/dist/language/selector-classes.d.ts +0 -1
  44. package/dist/language/selector-classes.js +0 -1
  45. package/dist/language/shell-language-options.d.ts +7 -1
  46. package/dist/language/shell-language-options.js +4 -4
  47. package/dist/language/shell-language.d.ts +10 -2
  48. package/dist/language/shell-language.js +59 -8
  49. package/dist/language/shell-source-code.d.ts +342 -5
  50. package/dist/language/shell-source-code.js +559 -30
  51. package/dist/language/source-coordinates.d.ts +13 -2
  52. package/dist/language/source-coordinates.js +68 -14
  53. package/dist/language/syntax-view.d.ts +60 -39
  54. package/dist/language/syntax-view.js +200 -45
  55. package/dist/language/word-resolution.d.ts +4 -3
  56. package/dist/language/word-resolution.js +4 -3
  57. package/dist/rules/compat/create-compat-rule.d.ts +59 -0
  58. package/dist/rules/compat/create-compat-rule.js +65 -0
  59. package/dist/rules/compat/declaration-options.d.ts +19 -0
  60. package/dist/rules/compat/declaration-options.js +110 -0
  61. package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
  62. package/dist/rules/compat/no-append-all-redirect.js +73 -0
  63. package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
  64. package/dist/rules/compat/no-associative-arrays.js +39 -0
  65. package/dist/rules/compat/no-case-attributes.d.ts +12 -0
  66. package/dist/rules/compat/no-case-attributes.js +63 -0
  67. package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
  68. package/dist/rules/compat/no-case-fallthrough.js +67 -0
  69. package/dist/rules/compat/no-case-modification.d.ts +35 -0
  70. package/dist/rules/compat/no-case-modification.js +86 -0
  71. package/dist/rules/compat/no-coproc.d.ts +12 -0
  72. package/dist/rules/compat/no-coproc.js +47 -0
  73. package/dist/rules/compat/no-declare-g.d.ts +12 -0
  74. package/dist/rules/compat/no-declare-g.js +59 -0
  75. package/dist/rules/compat/no-double-bracket.d.ts +12 -0
  76. package/dist/rules/compat/no-double-bracket.js +52 -0
  77. package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
  78. package/dist/rules/compat/no-epoch-variables.js +57 -0
  79. package/dist/rules/compat/no-fd-variable.d.ts +12 -0
  80. package/dist/rules/compat/no-fd-variable.js +48 -0
  81. package/dist/rules/compat/no-globstar.d.ts +12 -0
  82. package/dist/rules/compat/no-globstar.js +39 -0
  83. package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
  84. package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
  85. package/dist/rules/compat/no-let.d.ts +12 -0
  86. package/dist/rules/compat/no-let.js +62 -0
  87. package/dist/rules/compat/no-mapfile.d.ts +13 -0
  88. package/dist/rules/compat/no-mapfile.js +122 -0
  89. package/dist/rules/compat/no-nameref.d.ts +12 -0
  90. package/dist/rules/compat/no-nameref.js +44 -0
  91. package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
  92. package/dist/rules/compat/no-negative-array-index.js +123 -0
  93. package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
  94. package/dist/rules/compat/no-nounset-empty-array.js +158 -0
  95. package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
  96. package/dist/rules/compat/no-parameter-transformation.js +123 -0
  97. package/dist/rules/compat/no-pipe-all.d.ts +13 -0
  98. package/dist/rules/compat/no-pipe-all.js +68 -0
  99. package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
  100. package/dist/rules/compat/no-printf-time-format.js +99 -0
  101. package/dist/rules/compat/no-source.d.ts +13 -0
  102. package/dist/rules/compat/no-source.js +103 -0
  103. package/dist/rules/compat/no-test-v.d.ts +12 -0
  104. package/dist/rules/compat/no-test-v.js +280 -0
  105. package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
  106. package/dist/rules/compat/no-unicode-escape.js +120 -0
  107. package/dist/rules/compat/no-wait-n.d.ts +12 -0
  108. package/dist/rules/compat/no-wait-n.js +73 -0
  109. package/dist/rules/compat/operator-range.d.ts +10 -0
  110. package/dist/rules/compat/operator-range.js +15 -0
  111. package/dist/rules/compat/posix-absent-command.d.ts +12 -0
  112. package/dist/rules/compat/posix-absent-command.js +134 -0
  113. package/dist/rules/compat/shopt-options.d.ts +9 -0
  114. package/dist/rules/compat/shopt-options.js +63 -0
  115. package/dist/rules/compat/version-gates.d.ts +76 -0
  116. package/dist/rules/compat/version-gates.js +197 -0
  117. package/dist/rules/create-rule.d.ts +91 -0
  118. package/dist/rules/create-rule.js +51 -0
  119. package/dist/rules/create-wrapper-rule.d.ts +166 -0
  120. package/dist/rules/create-wrapper-rule.js +407 -0
  121. package/dist/rules/define-shell-rule.d.ts +184 -0
  122. package/dist/rules/define-shell-rule.js +56 -0
  123. package/dist/rules/index.d.ts +26 -0
  124. package/dist/rules/index.js +88 -0
  125. package/dist/rules/max-script-complexity.d.ts +28 -0
  126. package/dist/rules/max-script-complexity.js +130 -0
  127. package/dist/rules/no-dynamic-invocation.d.ts +35 -0
  128. package/dist/rules/no-dynamic-invocation.js +117 -0
  129. package/dist/rules/no-eval.d.ts +13 -0
  130. package/dist/rules/no-eval.js +89 -0
  131. package/dist/rules/no-masked-exit-status.d.ts +21 -0
  132. package/dist/rules/no-masked-exit-status.js +667 -0
  133. package/dist/rules/no-secret-env-reads.d.ts +24 -0
  134. package/dist/rules/no-secret-env-reads.js +133 -0
  135. package/dist/rules/no-secret-flags.d.ts +32 -0
  136. package/dist/rules/no-secret-flags.js +162 -0
  137. package/dist/rules/no-unchecked-cd.d.ts +44 -0
  138. package/dist/rules/no-unchecked-cd.js +250 -0
  139. package/dist/rules/no-undeclared-commands.d.ts +20 -0
  140. package/dist/rules/no-undeclared-commands.js +115 -0
  141. package/dist/rules/no-unused-declarations.d.ts +23 -0
  142. package/dist/rules/no-unused-declarations.js +259 -0
  143. package/dist/rules/prefer-local.d.ts +16 -0
  144. package/dist/rules/prefer-local.js +249 -0
  145. package/dist/rules/quote-expansions.d.ts +13 -0
  146. package/dist/rules/quote-expansions.js +229 -0
  147. package/dist/rules/require-network-wrapper.d.ts +40 -0
  148. package/dist/rules/require-network-wrapper.js +244 -0
  149. package/dist/rules/require-shebang.d.ts +29 -0
  150. package/dist/rules/require-shebang.js +101 -0
  151. package/dist/rules/require-strict-mode.d.ts +52 -0
  152. package/dist/rules/require-strict-mode.js +177 -0
  153. package/dist/rules/require-tty-guard.d.ts +13 -0
  154. package/dist/rules/require-tty-guard.js +289 -0
  155. package/dist/rules/resolve-declaration-convention.d.ts +96 -0
  156. package/dist/rules/resolve-declaration-convention.js +109 -0
  157. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  158. package/dist/rules/resolve-network-wrapper.js +116 -0
  159. package/dist/rules/set-options.d.ts +109 -0
  160. package/dist/rules/set-options.js +201 -0
  161. package/dist/rules/shebang-interpreter.d.ts +18 -0
  162. package/dist/rules/shebang-interpreter.js +81 -0
  163. package/dist/rules/shebang-matches-dialect.d.ts +12 -0
  164. package/dist/rules/shebang-matches-dialect.js +69 -0
  165. package/dist/rules/transparent-wrappers.d.ts +52 -0
  166. package/dist/rules/transparent-wrappers.js +99 -0
  167. package/dist/rules/user-pattern.d.ts +63 -0
  168. package/dist/rules/user-pattern.js +184 -0
  169. package/dist/syntax/analyze.d.ts +19 -0
  170. package/dist/syntax/analyze.js +21 -0
  171. package/dist/syntax/index.d.ts +23 -0
  172. package/dist/syntax/index.js +25 -0
  173. package/dist/version.d.ts +0 -1
  174. package/dist/version.js +0 -1
  175. package/docs/compat.md +347 -0
  176. package/docs/custom-rule-tutorial.md +780 -34
  177. package/docs/declaration-conventions.md +478 -0
  178. package/docs/examples/audit-wrapper-pack.mjs +15 -0
  179. package/docs/examples/dep-line-convention.mjs +22 -0
  180. package/docs/examples/eslint.config.mjs +3 -2
  181. package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
  182. package/docs/examples/no-backticks-rule-tester.mjs +106 -0
  183. package/docs/examples/no-backticks-rule.mjs +60 -0
  184. package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
  185. package/docs/examples/no-function-keyword-rule.mjs +59 -0
  186. package/docs/examples/no-listed-commands-rule.ts +63 -0
  187. package/docs/examples/no-rm-rf-rule.mjs +26 -15
  188. package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
  189. package/docs/examples/sidecar-convention.mjs +38 -0
  190. package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
  191. package/docs/examples/sorted-declarations-rule.mjs +52 -0
  192. package/docs/reading-nodes.md +159 -0
  193. package/docs/rules/README.md +78 -0
  194. package/docs/rules/_template.md +83 -0
  195. package/docs/rules/compat/no-append-all-redirect.md +87 -0
  196. package/docs/rules/compat/no-associative-arrays.md +95 -0
  197. package/docs/rules/compat/no-case-attributes.md +82 -0
  198. package/docs/rules/compat/no-case-fallthrough.md +90 -0
  199. package/docs/rules/compat/no-case-modification.md +74 -0
  200. package/docs/rules/compat/no-coproc.md +76 -0
  201. package/docs/rules/compat/no-declare-g.md +80 -0
  202. package/docs/rules/compat/no-double-bracket.md +72 -0
  203. package/docs/rules/compat/no-epoch-variables.md +79 -0
  204. package/docs/rules/compat/no-fd-variable.md +78 -0
  205. package/docs/rules/compat/no-globstar.md +74 -0
  206. package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
  207. package/docs/rules/compat/no-let.md +78 -0
  208. package/docs/rules/compat/no-mapfile.md +118 -0
  209. package/docs/rules/compat/no-nameref.md +81 -0
  210. package/docs/rules/compat/no-negative-array-index.md +87 -0
  211. package/docs/rules/compat/no-nounset-empty-array.md +103 -0
  212. package/docs/rules/compat/no-parameter-transformation.md +94 -0
  213. package/docs/rules/compat/no-pipe-all.md +85 -0
  214. package/docs/rules/compat/no-printf-time-format.md +75 -0
  215. package/docs/rules/compat/no-source.md +98 -0
  216. package/docs/rules/compat/no-test-v.md +82 -0
  217. package/docs/rules/compat/no-unicode-escape.md +91 -0
  218. package/docs/rules/compat/no-wait-n.md +85 -0
  219. package/docs/rules/max-script-complexity.md +109 -0
  220. package/docs/rules/no-dynamic-invocation.md +139 -0
  221. package/docs/rules/no-eval.md +106 -0
  222. package/docs/rules/no-masked-exit-status.md +239 -0
  223. package/docs/rules/no-secret-env-reads.md +173 -0
  224. package/docs/rules/no-secret-flags.md +169 -0
  225. package/docs/rules/no-unchecked-cd.md +146 -0
  226. package/docs/rules/no-undeclared-commands.md +141 -0
  227. package/docs/rules/no-unused-declarations.md +216 -0
  228. package/docs/rules/prefer-local.md +221 -0
  229. package/docs/rules/quote-expansions.md +157 -0
  230. package/docs/rules/require-network-wrapper.md +171 -0
  231. package/docs/rules/require-shebang.md +157 -0
  232. package/docs/rules/require-strict-mode.md +253 -0
  233. package/docs/rules/require-tty-guard.md +128 -0
  234. package/docs/rules/shebang-matches-dialect.md +120 -0
  235. package/docs/selectors.md +2 -2
  236. package/docs/shellcheck.md +86 -0
  237. package/docs/wrapper-rules.md +206 -0
  238. package/package.json +26 -20
  239. package/dist/index.d.ts.map +0 -1
  240. package/dist/index.js.map +0 -1
  241. package/dist/language/selector-classes.d.ts.map +0 -1
  242. package/dist/language/selector-classes.js.map +0 -1
  243. package/dist/language/shell-language-options.d.ts.map +0 -1
  244. package/dist/language/shell-language-options.js.map +0 -1
  245. package/dist/language/shell-language.d.ts.map +0 -1
  246. package/dist/language/shell-language.js.map +0 -1
  247. package/dist/language/shell-source-code.d.ts.map +0 -1
  248. package/dist/language/shell-source-code.js.map +0 -1
  249. package/dist/language/source-coordinates.d.ts.map +0 -1
  250. package/dist/language/source-coordinates.js.map +0 -1
  251. package/dist/language/syntax-view.d.ts.map +0 -1
  252. package/dist/language/syntax-view.js.map +0 -1
  253. package/dist/language/word-resolution.d.ts.map +0 -1
  254. package/dist/language/word-resolution.js.map +0 -1
  255. package/dist/version.d.ts.map +0 -1
  256. 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
+ });