@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,667 @@
1
+ /**
2
+ * `sh/no-masked-exit-status`: a declaration builtin whose value is a command
3
+ * substitution reports the builtin's exit status, not the command's.
4
+ *
5
+ * `local x=$(cmd)`, `export X=$(cmd)`, `readonly X=$(cmd)`, `declare x=$(cmd)`
6
+ * and `typeset x=$(cmd)` all succeed whatever `cmd` did, so `set -e` does not
7
+ * stop the script and `… && next` runs `next` anyway. A script that turns
8
+ * strict mode on still swallows the failure there (shellcheck SC2155).
9
+ *
10
+ * **What is reported.** A `local`, `declare`, `typeset`, `export` or
11
+ * `readonly` operand of the form `name=value` whose value contains a command
12
+ * substitution (`$(…)` or backticks) at any depth of the word: inside double
13
+ * quotes, next to other text, inside a parameter expansion's operand, a
14
+ * substring offset or an array subscript, and inside an arithmetic expansion.
15
+ * Real shells give a plain assignment the status of a substitution in any of
16
+ * those places, so the declaration hides it from all of them. A process
17
+ * substitution (`<(…)`) is not looked into: it expands to a path. The builtin
18
+ * is also read behind the transparent wrappers `builtin` and `command`. One
19
+ * report is made per operand, on the operand. An array value
20
+ * (`local -a a=($(cmd))`) is not reported: shellcheck 0.11.0 does not report
21
+ * it either, and a status-bearing scalar is the construct the rule exists for.
22
+ *
23
+ * Not reported: a value with no substitution, a declaration with no value,
24
+ * a plain assignment (`x=$(cmd)` keeps the command's status), and the split
25
+ * form `local x; x=$(cmd)`.
26
+ *
27
+ * **Options.** `builtins` limits which of the five builtins are checked.
28
+ * `onlyWithErrexit` reports only where `errexit` is known to be on, except
29
+ * where the script reads the status explicitly: the left side of `&&` / `||`,
30
+ * the right side of one that is itself read (the middle of `a && b && c`), and
31
+ * the last command of an `if`/`elif`/`while`/`until` condition. There the
32
+ * status is the builtin's whatever the options. `errexit` is known to be on
33
+ * for a statement when the plain top-level `set` statements before it (read
34
+ * with `require-strict-mode`'s `set` reading) leave it on, wherever they sit,
35
+ * and, with `allowShebangOnly`, when the shebang line enables it. A function
36
+ * body is read at the point it is defined. Under the `bats` dialect a
37
+ * statement written directly in an `@test` body or a top-level `setup` /
38
+ * `teardown` hook is on, as in `no-unchecked-cd`. A `set` inside a function,
39
+ * a group or a condition, in a sourced file, or `set -e` on a command line
40
+ * (`bash -e script.sh`) is not seen.
41
+ *
42
+ * **No autofix, only a suggestion.** Splitting the declaration changes
43
+ * behavior on purpose: under `set -e` a failure now stops the script. The
44
+ * suggestion declares the names first and assigns each substituted one on its
45
+ * own statement, keeping the builtin's flags; `export` assigns first and
46
+ * exports afterwards, because exporting first would put the old value in the
47
+ * substitution's environment. It is withheld where splitting
48
+ * would not be equivalent, would change the surrounding list, or would turn
49
+ * invalid input into a valid assignment:
50
+ *
51
+ * - the value reads a name the split moves before it. For `local`, `declare`
52
+ * and `typeset` inside a function that includes the variable itself (the
53
+ * declaration creates an empty local, so the old value the substitution
54
+ * read is gone); for every builtin it includes the other operands of the
55
+ * same declaration, which the split assigns earlier or later than before.
56
+ * Any mention of such a name in the value counts, because a command can
57
+ * read a variable by name (`[ -v x ]`, `declare -p x`, `printenv x`), and
58
+ * under `-i` the builtin evaluates values as arithmetic, and `export` runs
59
+ * after the assignments, so both reach the values without a substitution;
60
+ * - a scoped declaration of a variable the shell or its commands consult
61
+ * (`local PATH=$(cmd)`): the declaration empties it before the command runs;
62
+ * - code the rule cannot read may read such a name: the value runs `eval`,
63
+ * `source` or `.`, a command whose name is not static, or a function
64
+ * defined in this file. This applies where the split changes what is
65
+ * visible: `local`, `declare` or `typeset` inside a function, several
66
+ * operands, or any flag. A function defined in another file cannot be seen,
67
+ * so a call to it is not detected;
68
+ * - the statement has prefix assignments (`A=1 export x=$(cmd)`), which the
69
+ * replacement would delete;
70
+ * - a flag changes the meaning of the split or is not valid with an
71
+ * assignment: `-g` (the split writes the local, not the global), `-x` on
72
+ * `local`, `declare` or `typeset` (declaring first exports the name to the
73
+ * substitution), `-n` on those builtins (the assignment writes through a
74
+ * nameref that already exists), `-f`, `-F`, `-p`, `export -x`, or any flag
75
+ * not static;
76
+ * - `local` outside a function, which is an error;
77
+ * - the declaration is not a standalone statement: an operand of `&&` / `||`
78
+ * or a pipeline, a condition, negated, backgrounded, redirected, or the
79
+ * body of a function written without braces;
80
+ * - the declaration is reached through `builtin` or `command`;
81
+ * - an operand is an append (`+=`) or has a subscript (`a[1]=…`).
82
+ *
83
+ * `-r` (and `readonly` itself) forbids a later assignment, so those forms are
84
+ * split into the declaration, the assignment, then `readonly <names>`.
85
+ */
86
+ import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
87
+ import { childNodes } from '../language/syntax-view.js';
88
+ import { lineBreakOf } from '../language/line-breaks.js';
89
+ import { createRule } from './create-rule.js';
90
+ import { batsErrexitIsOn, isStatusChecked } from './no-unchecked-cd.js';
91
+ import { setStatementChanges } from './require-strict-mode.js';
92
+ import { applySetOptionChanges, shebangOptionChanges } from './set-options.js';
93
+ import { BUILTIN_WRAPPER } from './transparent-wrappers.js';
94
+ /** Every declaration builtin the rule can check; the default for `builtins`. */
95
+ const ALL_BUILTINS = ['local', 'declare', 'typeset', 'export', 'readonly'];
96
+ /** The builtins that create a variable in the enclosing function's scope. */
97
+ const SCOPED_BUILTINS = new Set(['local', 'declare', 'typeset']);
98
+ /**
99
+ * Variables the shell or the commands it runs consult. A scoped declaration
100
+ * empties them for the substitution that the split moves after it, so
101
+ * `local PATH; PATH=$(cmd)` cannot even find `cmd`. An exported variable of any
102
+ * other name can be read by a command too, but that is not visible here.
103
+ */
104
+ const CONSULTED_VARIABLES = new Set([
105
+ 'BASH_ENV',
106
+ 'CDPATH',
107
+ 'ENV',
108
+ 'GLOBIGNORE',
109
+ 'HOME',
110
+ 'IFS',
111
+ 'LANG',
112
+ 'LANGUAGE',
113
+ 'LC_ALL',
114
+ 'LC_COLLATE',
115
+ 'LC_CTYPE',
116
+ 'LC_MESSAGES',
117
+ 'LC_NUMERIC',
118
+ 'LC_TIME',
119
+ 'LD_LIBRARY_PATH',
120
+ 'OLDPWD',
121
+ 'PATH',
122
+ 'POSIXLY_CORRECT',
123
+ 'PWD',
124
+ 'SHELL',
125
+ 'TERM',
126
+ 'TMPDIR',
127
+ 'TZ',
128
+ 'USER',
129
+ ]);
130
+ /** A valid shell variable name. */
131
+ const VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
132
+ /**
133
+ * Wrappers followed to find the declaration builtin: `command` (with its
134
+ * `-p`) and `builtin`. Other wrappers (`sudo`, `env`) run external programs,
135
+ * which cannot be declaration builtins.
136
+ */
137
+ const TRANSPARENT_WRAPPERS = [
138
+ ...DEFAULT_TRANSPARENT_WRAPPERS.filter((spec) => spec.names.includes('command')),
139
+ BUILTIN_WRAPPER,
140
+ ];
141
+ /**
142
+ * Whether `node` contains a command substitution whose status the declaration
143
+ * would replace. A substitution counts wherever it sits in the word, including
144
+ * inside an arithmetic expansion, a substring offset or an array subscript:
145
+ * in a plain assignment a failing substitution there fails the assignment
146
+ * (and so trips `set -e`) in Bash, dash, zsh and ksh93. A process
147
+ * substitution is not entered: it expands to a path, so there is no status.
148
+ */
149
+ function containsCommandSubstitution(node) {
150
+ if (node.type === 'CmdSubst')
151
+ return true;
152
+ if (node.type === 'ProcSubst')
153
+ return false;
154
+ return childNodes(node).some(containsCommandSubstitution);
155
+ }
156
+ /** Whether `text` mentions `name` as a whole identifier. */
157
+ function mentionsIdentifier(text, name) {
158
+ return new RegExp(`(?<![A-Za-z0-9_])${name}(?![A-Za-z0-9_])`, 'u').test(text);
159
+ }
160
+ /**
161
+ * Whether evaluating `node` reads any of `names`: a parameter expansion of
162
+ * the name (`$x`, `${x:-…}`, `${a[x]}`), or an arithmetic expansion that
163
+ * mentions it as a bare name (`$((x + 1))`), or an indirect expansion
164
+ * (`${!ref}`), which can read any name. Command substitutions are entered, so
165
+ * `$(echo "$x")` counts. Reads made by code the substitution runs are
166
+ * {@link runsUnseenCode}'s concern.
167
+ */
168
+ function readsAnyName(node, names, sourceCode) {
169
+ if (names.length === 0)
170
+ return false;
171
+ if (node.type === 'ParamExp') {
172
+ if (node.excl)
173
+ return true;
174
+ const param = node.param?.value;
175
+ if (param !== undefined && names.includes(param))
176
+ return true;
177
+ if (node.index !== undefined) {
178
+ const indexText = sourceCode.text.slice(node.index.range[0], node.index.range[1]);
179
+ if (names.some((name) => mentionsIdentifier(indexText, name)))
180
+ return true;
181
+ }
182
+ }
183
+ if (node.type === 'ArithmExp' || node.type === 'ArithmCmd') {
184
+ const arithmetic = sourceCode.text.slice(node.range[0], node.range[1]);
185
+ if (names.some((name) => mentionsIdentifier(arithmetic, name)))
186
+ return true;
187
+ }
188
+ return childNodes(node).some((child) => readsAnyName(child, names, sourceCode));
189
+ }
190
+ /**
191
+ * Whether `node` sits inside a function body: a function definition or a bats
192
+ * `@test` block, which bats runs as a function.
193
+ */
194
+ function insideFunction(node, sourceCode) {
195
+ for (let up = sourceCode.getParent(node); up !== undefined; up = sourceCode.getParent(up)) {
196
+ if (up.type === 'FuncDecl' || up.type === 'TestDecl')
197
+ return true;
198
+ }
199
+ return false;
200
+ }
201
+ /**
202
+ * Builtins that run code the rule cannot read: they evaluate a string or a
203
+ * file, so they may read any variable.
204
+ */
205
+ const CODE_RUNNING_BUILTINS = new Set(['eval', 'source', '.']);
206
+ /** The names of the functions defined anywhere in the file, at any depth. */
207
+ function collectFunctionNames(node, into) {
208
+ if (node.type === 'FuncDecl' && node.name !== undefined)
209
+ into.add(node.name.value);
210
+ for (const child of childNodes(node))
211
+ collectFunctionNames(child, into);
212
+ return into;
213
+ }
214
+ /**
215
+ * Whether evaluating `node` may run code that reads shell variables without
216
+ * showing it: `eval`, `source` / `.`, a command whose name is not static, or
217
+ * a call to a function defined in this file. The split moves a declaration
218
+ * earlier, so such code can see a different variable afterwards. A function
219
+ * defined in another file cannot be seen and is not detected.
220
+ */
221
+ function runsUnseenCode(node, functionNames, sourceCode) {
222
+ if (node.type === 'CallExpr' && node.args.length > 0) {
223
+ const invoked = sourceCode.resolveInvokedCommand(node, {
224
+ transparentWrappers: TRANSPARENT_WRAPPERS,
225
+ });
226
+ if (invoked?.effective.static !== true)
227
+ return true;
228
+ const name = invoked.effective.text;
229
+ if (CODE_RUNNING_BUILTINS.has(name) || functionNames.has(name))
230
+ return true;
231
+ }
232
+ return childNodes(node).some((child) => runsUnseenCode(child, functionNames, sourceCode));
233
+ }
234
+ /**
235
+ * The state of `errexit` before each top-level statement of `file`, from the
236
+ * shebang (when `allowShebangOnly`) and the plain top-level `set` statements
237
+ * before it, read in order with `require-strict-mode`'s own `set` reading.
238
+ * Unlike that rule there is no statement window: a `set -e` at any position
239
+ * counts for the statements after it. A `set` inside a function, a group, a
240
+ * condition or a sourced file is not seen.
241
+ */
242
+ function errexitBeforeEachTopLevelStatement(file, sourceCode, allowShebangOnly) {
243
+ const enabled = applySetOptionChanges(new Set(), allowShebangOnly ? shebangOptionChanges(sourceCode.text) : []);
244
+ const before = new Map();
245
+ for (const stmt of file.stmts) {
246
+ before.set(stmt, enabled.has('errexit'));
247
+ applySetOptionChanges(enabled, setStatementChanges(stmt, sourceCode));
248
+ }
249
+ return before;
250
+ }
251
+ /**
252
+ * Whether `stmt` is a whole statement of a statement list, so replacing its
253
+ * command with several statements changes nothing about the list around it.
254
+ */
255
+ function isStandalone(stmt, sourceCode) {
256
+ if (stmt.background || stmt.negated || stmt.redirs.length > 0)
257
+ return false;
258
+ const parent = sourceCode.getParent(stmt);
259
+ switch (parent?.type) {
260
+ case 'File':
261
+ case 'Block':
262
+ case 'Subshell':
263
+ case 'CaseItem':
264
+ return true;
265
+ case 'IfClause':
266
+ case 'WhileClause':
267
+ return !parent.cond.includes(stmt);
268
+ case 'ForClause':
269
+ return parent.do.includes(stmt);
270
+ default:
271
+ return false;
272
+ }
273
+ }
274
+ /** The operand text a declaration keeps for `operand`: its bare name when substituted. */
275
+ function keptText(operand) {
276
+ return operand.substituted ? operand.name : operand.text;
277
+ }
278
+ /** Reads `name=value` operands of an `Assign` list (a parsed `DeclClause`). */
279
+ function operandsOfDeclClause(node, sourceCode) {
280
+ const flags = [];
281
+ const operands = [];
282
+ let flagsKnown = true;
283
+ for (const arg of node.args) {
284
+ if (arg.name === undefined) {
285
+ const flag = arg.value === undefined ? null : sourceCode.resolveWordText(arg.value);
286
+ if (flag === null)
287
+ flagsKnown = false;
288
+ else
289
+ flags.push(flag);
290
+ continue;
291
+ }
292
+ const substituted = !arg.naked &&
293
+ arg.array === undefined &&
294
+ arg.value !== undefined &&
295
+ containsCommandSubstitution(arg.value);
296
+ operands.push({
297
+ node: arg,
298
+ name: arg.name.value,
299
+ substituted,
300
+ values: arg.value === undefined ? [] : [arg.value],
301
+ text: sourceCode.text.slice(arg.range[0], arg.range[1]),
302
+ unsplittable: arg.append || arg.index !== undefined,
303
+ });
304
+ }
305
+ return { variant: node.variant?.value ?? '', flags, operands, flagsKnown };
306
+ }
307
+ /**
308
+ * Reads `word` as `name=value`, `name+=value` or `name[subscript]=value`, or
309
+ * `null` when it is not an assignment. The text before the `=` can span parts:
310
+ * the parser may split `w[1]=` into two literals, and a subscript may hold
311
+ * expansions and quotes, which are parts of their own, so only literal parts
312
+ * are scanned for the bracket that closes it. A substitution in the subscript
313
+ * is not part of the value, as in a parsed `Assign`.
314
+ */
315
+ function assignmentOfWord(word) {
316
+ const first = word.parts.at(0);
317
+ if (first?.type !== 'Lit')
318
+ return null;
319
+ const name = /^[A-Za-z_][A-Za-z0-9_]*/u.exec(first.value)?.[0];
320
+ if (name === undefined)
321
+ return null;
322
+ let depth = 0;
323
+ let subscripted = false;
324
+ for (const [index, part] of word.parts.entries()) {
325
+ if (part.type !== 'Lit') {
326
+ if (depth === 0)
327
+ return null;
328
+ continue;
329
+ }
330
+ for (let at = index === 0 ? name.length : 0; at < part.value.length; at += 1) {
331
+ const character = part.value[at];
332
+ if (depth > 0) {
333
+ if (character === '\\')
334
+ at += 1;
335
+ else if (character === '[')
336
+ depth += 1;
337
+ else if (character === ']')
338
+ depth -= 1;
339
+ }
340
+ else if (character === '[') {
341
+ subscripted = true;
342
+ depth = 1;
343
+ }
344
+ else {
345
+ const append = part.value.startsWith('+=', at);
346
+ if (character !== '=' && !append)
347
+ return null;
348
+ return {
349
+ name,
350
+ values: word.parts.slice(index + 1),
351
+ unsplittable: subscripted || append,
352
+ };
353
+ }
354
+ }
355
+ }
356
+ return null;
357
+ }
358
+ /**
359
+ * Reads a declaration builtin whose operands the parser left as ordinary
360
+ * words: under the `posix` dialect (which has no declaration clause) and
361
+ * behind `builtin` / `command`. A word {@link assignmentOfWord} recognizes
362
+ * is an assignment operand; a static word starting with `-` is
363
+ * a flag; a static bare name is a valueless operand. Any other word (a
364
+ * dynamic one, say) makes the flags unknown, which only withholds the
365
+ * suggestion.
366
+ */
367
+ function declarationOfWords(variant, words, sourceCode) {
368
+ const flags = [];
369
+ const operands = [];
370
+ let flagsKnown = true;
371
+ for (const word of words) {
372
+ const text = sourceCode.text.slice(word.range[0], word.range[1]);
373
+ const assignment = assignmentOfWord(word);
374
+ if (assignment !== null) {
375
+ operands.push({
376
+ node: word,
377
+ name: assignment.name,
378
+ substituted: assignment.values.some(containsCommandSubstitution),
379
+ values: assignment.values,
380
+ text,
381
+ unsplittable: assignment.unsplittable,
382
+ });
383
+ continue;
384
+ }
385
+ const resolved = sourceCode.resolveWordText(word);
386
+ if (resolved !== null && VARIABLE_NAME.test(resolved)) {
387
+ operands.push({
388
+ node: word,
389
+ name: resolved,
390
+ substituted: false,
391
+ values: [],
392
+ text,
393
+ unsplittable: false,
394
+ });
395
+ }
396
+ else if (resolved?.startsWith('-') === true) {
397
+ flags.push(resolved);
398
+ }
399
+ else {
400
+ flagsKnown = false;
401
+ }
402
+ }
403
+ return { variant, flags, operands, flagsKnown };
404
+ }
405
+ /**
406
+ * The option letters each builtin accepts together with a `name=value`
407
+ * operand, restricted to ones that mean the same in the split form. Anything
408
+ * else (`-g`, which retargets the assignment to the global scope; `-x`, which
409
+ * would export the name to the substitution when declared first; `-n`, whose
410
+ * later assignment writes through a nameref that already exists instead of
411
+ * rebinding it; `-f`, `-F` and `-p`, which do not take assignments; an option
412
+ * the builtin does not have, such as `export -x`) withholds the suggestion, so it never turns an
413
+ * error into an assignment or moves a write to another scope.
414
+ */
415
+ const VALID_FLAG_LETTERS = {
416
+ local: 'aAilrtu',
417
+ declare: 'aAilrtu',
418
+ typeset: 'aAilrtu',
419
+ export: 'n',
420
+ readonly: '',
421
+ };
422
+ /** Whether every flag word is a short-option cluster of letters valid for `variant`. */
423
+ function flagsAreValid(variant, flags) {
424
+ const letters = VALID_FLAG_LETTERS[variant] ?? '';
425
+ return flags.every((flag) => new RegExp(`^-[${letters}]+$`, 'u').test(flag));
426
+ }
427
+ /** Whether a flag word is a short-option cluster that includes `-i` (integer). */
428
+ function isIntegerFlag(flag) {
429
+ return /^-[A-Za-z]*i[A-Za-z]*$/u.test(flag);
430
+ }
431
+ /** The text after the first `=` of an operand, or `''` for a bare name. */
432
+ function valueText(operand) {
433
+ const equals = operand.text.indexOf('=');
434
+ return equals === -1 ? '' : operand.text.slice(equals + 1);
435
+ }
436
+ /** Whether a flag word is a short-option cluster that includes `-r`. */
437
+ function isReadonlyFlag(flag) {
438
+ return /^-[A-Za-z]*r[A-Za-z]*$/u.test(flag);
439
+ }
440
+ /** `flag` without its `r` letter, or `null` when only the dash would be left. */
441
+ function withoutReadonly(flag) {
442
+ const remaining = flag.replace('r', '');
443
+ return remaining === '-' ? null : remaining;
444
+ }
445
+ /**
446
+ * The replacement text for a standalone declaration, or `null` where no
447
+ * equivalent split exists. `separator` joins the statements.
448
+ */
449
+ function splitText(declaration, scoped, separator, functionNames, sourceCode) {
450
+ const { variant, flags, operands, flagsKnown } = declaration;
451
+ if (!flagsKnown || operands.some((operand) => operand.unsplittable))
452
+ return null;
453
+ const isReadonlyBuiltin = variant === 'readonly';
454
+ if (isReadonlyBuiltin && flags.length > 0)
455
+ return null;
456
+ if (!flagsAreValid(variant, flags))
457
+ return null;
458
+ if (scoped && operands.some((operand) => CONSULTED_VARIABLES.has(operand.name)))
459
+ return null;
460
+ const names = operands.map((operand) => operand.name);
461
+ // The names a value could read that the split changes: its own when the
462
+ // declaration creates a new scope, and the other operands'.
463
+ const namesAffectingValueOf = (operand) => scoped ? names : names.filter((name) => name !== operand.name);
464
+ for (const operand of operands) {
465
+ if (!operand.substituted)
466
+ continue;
467
+ const others = namesAffectingValueOf(operand);
468
+ if (operand.values.some((value) => readsAnyName(value, others, sourceCode)))
469
+ return null;
470
+ // A command can read a variable by name, with no expansion to see:
471
+ // `[ -v x ]`, `declare -p x`, `printenv x`.
472
+ if (others.some((name) => mentionsIdentifier(valueText(operand), name)))
473
+ return null;
474
+ }
475
+ // Operands without a substitution count in two cases. Under `-i` the builtin
476
+ // evaluates each value as arithmetic, where a bare identifier reads a
477
+ // variable. And `export` runs after the assignments, so its other values
478
+ // would read the new value (`export A=$(c) B=$A`).
479
+ if (variant === 'export' || flags.some(isIntegerFlag)) {
480
+ for (const operand of operands) {
481
+ const text = valueText(operand);
482
+ if (namesAffectingValueOf(operand).some((name) => mentionsIdentifier(text, name))) {
483
+ return null;
484
+ }
485
+ }
486
+ }
487
+ // Code the split cannot see into may read a name that the split declares
488
+ // earlier or in a new scope. A lone, flagless `export` or `readonly`
489
+ // operand declares nothing that code could observe differently.
490
+ const mayObserveDeclaration = scoped || operands.length > 1 || flags.length > 0;
491
+ if (mayObserveDeclaration &&
492
+ operands.some((operand) => operand.values.some((value) => runsUnseenCode(value, functionNames, sourceCode)))) {
493
+ return null;
494
+ }
495
+ const makesReadonly = isReadonlyBuiltin || flags.some(isReadonlyFlag);
496
+ const keptFlags = flags.flatMap((flag) => {
497
+ if (!isReadonlyFlag(flag))
498
+ return [flag];
499
+ const stripped = withoutReadonly(flag);
500
+ return stripped === null ? [] : [stripped];
501
+ });
502
+ const substituted = operands.filter((operand) => operand.substituted);
503
+ // `export` creates no scope, so nothing needs it to come first, and coming
504
+ // first would export the old value to the substitution.
505
+ if (variant === 'export') {
506
+ return {
507
+ text: [
508
+ ...substituted.map((operand) => operand.text),
509
+ [variant, ...flags, ...operands.map(keptText)].join(' '),
510
+ ].join(separator),
511
+ messageId: 'assignThenExport',
512
+ };
513
+ }
514
+ const statements = [];
515
+ const declared = isReadonlyBuiltin
516
+ ? operands.filter((operand) => !operand.substituted)
517
+ : operands;
518
+ if (declared.length > 0) {
519
+ statements.push([variant, ...keptFlags, ...declared.map(keptText)].join(' '));
520
+ }
521
+ for (const operand of substituted)
522
+ statements.push(operand.text);
523
+ if (makesReadonly) {
524
+ const readonlyNames = isReadonlyBuiltin ? substituted : operands;
525
+ statements.push(`readonly ${readonlyNames.map((operand) => operand.name).join(' ')}`);
526
+ }
527
+ return {
528
+ text: statements.join(separator),
529
+ messageId: makesReadonly ? 'assignThenReadonly' : 'declareThenAssign',
530
+ };
531
+ }
532
+ /** Built-in rule `sh/no-masked-exit-status`. @internal */
533
+ export const noMaskedExitStatus = createRule({
534
+ name: 'no-masked-exit-status',
535
+ meta: {
536
+ type: 'problem',
537
+ docs: {
538
+ description: 'disallow a declaration builtin (`local`, `export`, …) assigned from a command substitution, which hides the command’s exit status',
539
+ },
540
+ hasSuggestions: true,
541
+ schema: [
542
+ {
543
+ type: 'object',
544
+ properties: {
545
+ onlyWithErrexit: { type: 'boolean' },
546
+ allowShebangOnly: { type: 'boolean' },
547
+ builtins: {
548
+ type: 'array',
549
+ items: { enum: [...ALL_BUILTINS] },
550
+ minItems: 1,
551
+ uniqueItems: true,
552
+ },
553
+ },
554
+ additionalProperties: false,
555
+ },
556
+ ],
557
+ defaultOptions: [
558
+ { onlyWithErrexit: false, allowShebangOnly: false, builtins: [...ALL_BUILTINS] },
559
+ ],
560
+ messages: {
561
+ maskedExitStatus: '`{{builtin}}` returns its own exit status instead of the command substitution’s in the value of `{{name}}`, so neither `set -e` nor `&&` sees the command fail. Assign `{{name}}` in a separate statement from `{{builtin}}`.',
562
+ declareThenAssign: 'Declare `{{names}}` first and assign it in a separate statement.',
563
+ assignThenExport: 'Assign `{{names}}` in a separate statement, then export it.',
564
+ assignThenReadonly: 'Declare and assign `{{names}}` in separate statements, then make it read-only.',
565
+ },
566
+ },
567
+ create(context) {
568
+ const [options] = context.options;
569
+ const { sourceCode } = context;
570
+ const checked = new Set(options.builtins ?? ALL_BUILTINS);
571
+ const parentOf = (node) => sourceCode.getParent(node);
572
+ let errexitBefore = new Map();
573
+ let functionNames = new Set();
574
+ /**
575
+ * Whether `errexit` is known to be on when `stmt` runs: the statement is
576
+ * written directly in a bats `@test` body or hook, or the plain top-level
577
+ * `set` statements before the top-level statement that contains it leave
578
+ * it on. A function body is read at its definition.
579
+ */
580
+ const errexitIsOn = (stmt) => {
581
+ if (sourceCode.dialect === 'bats' && batsErrexitIsOn(stmt, parentOf, sourceCode))
582
+ return true;
583
+ let topLevel = stmt;
584
+ for (let up = parentOf(topLevel); up !== undefined && up.type !== 'File'; up = parentOf(up)) {
585
+ topLevel = up;
586
+ }
587
+ return errexitBefore.get(topLevel) === true;
588
+ };
589
+ /** Reports each substituted operand of `declaration`, with a suggestion where one is safe. */
590
+ const check = (node, builtin, declaration, offerSuggestion) => {
591
+ if (!checked.has(builtin))
592
+ return;
593
+ const substituted = declaration.operands.filter((operand) => operand.substituted);
594
+ if (substituted.length === 0)
595
+ return;
596
+ const stmt = sourceCode.getParent(node);
597
+ if (stmt?.type !== 'Stmt')
598
+ return;
599
+ if (options.onlyWithErrexit === true &&
600
+ !isStatusChecked(stmt, parentOf) &&
601
+ !errexitIsOn(stmt)) {
602
+ return;
603
+ }
604
+ let suggestion = null;
605
+ const inFunction = insideFunction(node, sourceCode);
606
+ // `local` outside a function is an error, which a split would turn into
607
+ // an assignment of a global.
608
+ const validHere = builtin !== 'local' || inFunction;
609
+ if (offerSuggestion && validHere && isStandalone(stmt, sourceCode)) {
610
+ const text = sourceCode.text;
611
+ const start = node.range[0];
612
+ const lineStart = Math.max(text.lastIndexOf('\n', start - 1), text.lastIndexOf('\r', start - 1));
613
+ const indent = text.slice(lineStart + 1, start);
614
+ const separator = /^[ \t]*$/u.test(indent) ? lineBreakOf(text) + indent : '; ';
615
+ const scoped = SCOPED_BUILTINS.has(builtin) && inFunction;
616
+ suggestion = splitText(declaration, scoped, separator, functionNames, sourceCode);
617
+ }
618
+ for (const operand of substituted) {
619
+ context.report({
620
+ node: operand.node,
621
+ messageId: 'maskedExitStatus',
622
+ data: { builtin, name: operand.name },
623
+ suggest: suggestion === null
624
+ ? []
625
+ : [
626
+ {
627
+ messageId: suggestion.messageId,
628
+ data: { names: substituted.map((s) => s.name).join(' ') },
629
+ fix: (fixer) => fixer.replaceTextRange(node.range, suggestion.text),
630
+ },
631
+ ],
632
+ });
633
+ }
634
+ };
635
+ return {
636
+ File(node) {
637
+ functionNames = collectFunctionNames(node, new Set());
638
+ if (options.onlyWithErrexit === true) {
639
+ errexitBefore = errexitBeforeEachTopLevelStatement(node, sourceCode, options.allowShebangOnly === true);
640
+ }
641
+ },
642
+ DeclClause(node) {
643
+ const declaration = operandsOfDeclClause(node, sourceCode);
644
+ check(node, declaration.variant, declaration, true);
645
+ },
646
+ CallExpr(node) {
647
+ const invoked = sourceCode.resolveInvokedCommand(node, {
648
+ transparentWrappers: TRANSPARENT_WRAPPERS,
649
+ });
650
+ if (invoked?.effective.static !== true)
651
+ return;
652
+ const builtin = invoked.effective.text;
653
+ if (!ALL_BUILTINS.includes(builtin))
654
+ return;
655
+ const effectiveIndex = invoked.chainIndices.at(-1);
656
+ if (effectiveIndex === undefined)
657
+ return;
658
+ const declaration = declarationOfWords(builtin, node.args.slice(effectiveIndex + 1), sourceCode);
659
+ // A suggestion rewrites the whole command, which is only safe to
660
+ // rebuild when no wrapper precedes the builtin.
661
+ // Prefix assignments (`A=1 export x=$(c)`) belong to the command the
662
+ // replacement overwrites, so they too withhold the suggestion.
663
+ check(node, builtin, declaration, invoked.chain.length === 1 && node.assigns.length === 0);
664
+ },
665
+ };
666
+ },
667
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The rule's single options object.
3
+ *
4
+ * @internal
5
+ */
6
+ export interface NoSecretEnvReadsOptions {
7
+ /**
8
+ * Regex sources tested against each expanded variable's name. Replaces
9
+ * {@link DEFAULT_PATTERNS} entirely.
10
+ */
11
+ readonly patterns?: readonly string[];
12
+ /**
13
+ * Command names whose arguments may read a secret-shaped variable. A
14
+ * command run by path matches by basename.
15
+ */
16
+ readonly allowedSinks?: readonly string[];
17
+ }
18
+ /**
19
+ * The `sh/no-secret-env-reads` rule module, registered as
20
+ * `no-secret-env-reads`.
21
+ *
22
+ * @internal
23
+ */
24
+ export declare const noSecretEnvReads: import("./define-shell-rule.js").ShellRule<[NoSecretEnvReadsOptions?], "secretEnvRead">;