@eslint-sh/eslint-plugin 0.6.0 → 0.8.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 (147) hide show
  1. package/README.md +19 -8
  2. package/dist/command-harvest/builtins/bash.d.ts +1 -1
  3. package/dist/command-harvest/builtins/bash.js +1 -1
  4. package/dist/command-harvest/builtins/bats.d.ts +2 -0
  5. package/dist/command-harvest/builtins/bats.js +3 -0
  6. package/dist/command-harvest/wrapper-table.d.ts +3 -19
  7. package/dist/command-harvest/wrapper-table.js +8 -38
  8. package/dist/configs/bash-3-2.d.ts +7 -6
  9. package/dist/configs/bash-3-2.js +5 -3
  10. package/dist/configs/posix-portable.d.ts +1 -1
  11. package/dist/configs/posix-portable.js +1 -0
  12. package/dist/declaration-conventions/read-declared-commands.d.ts +2 -2
  13. package/dist/declaration-conventions/read-declared-commands.js +2 -2
  14. package/dist/eslint-plugin-syntax.d.ts +4 -0
  15. package/dist/eslint-plugin.d.ts +86 -40
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.js +4 -2
  18. package/dist/language/shell-language-options.d.ts +18 -15
  19. package/dist/language/shell-language-options.js +33 -26
  20. package/dist/language/shell-language.d.ts +10 -9
  21. package/dist/language/shell-language.js +18 -35
  22. package/dist/language/shell-source-code.d.ts +17 -9
  23. package/dist/language/shell-source-code.js +53 -12
  24. package/dist/rules/bash-variable-semantics.d.ts +22 -0
  25. package/dist/rules/bash-variable-semantics.js +41 -0
  26. package/dist/rules/compat/create-compat-rule.js +1 -1
  27. package/dist/rules/compat/declaration-options.d.ts +2 -2
  28. package/dist/rules/compat/declaration-options.js +5 -2
  29. package/dist/rules/compat/no-append-all-redirect.js +1 -1
  30. package/dist/rules/compat/no-associative-arrays.js +2 -2
  31. package/dist/rules/compat/no-brace-sequence-padding-step.d.ts +15 -0
  32. package/dist/rules/compat/no-brace-sequence-padding-step.js +287 -0
  33. package/dist/rules/compat/no-case-attributes.js +3 -3
  34. package/dist/rules/compat/no-case-fallthrough.js +2 -2
  35. package/dist/rules/compat/no-case-modification.js +2 -2
  36. package/dist/rules/compat/no-coproc.js +1 -1
  37. package/dist/rules/compat/no-declare-g.js +3 -3
  38. package/dist/rules/compat/no-double-bracket.js +2 -2
  39. package/dist/rules/compat/no-epoch-variables.js +2 -2
  40. package/dist/rules/compat/no-fd-variable.js +1 -1
  41. package/dist/rules/compat/no-globstar.js +1 -1
  42. package/dist/rules/compat/no-lastpipe-semantics.js +2 -2
  43. package/dist/rules/compat/no-let.js +2 -1
  44. package/dist/rules/compat/no-mapfile.js +4 -2
  45. package/dist/rules/compat/no-nameref.js +3 -3
  46. package/dist/rules/compat/no-negative-array-index.js +39 -9
  47. package/dist/rules/compat/no-nounset-empty-array.js +15 -4
  48. package/dist/rules/compat/no-parameter-transformation.js +2 -2
  49. package/dist/rules/compat/no-pipe-all.js +1 -1
  50. package/dist/rules/compat/no-printf-time-format.js +4 -2
  51. package/dist/rules/compat/no-source.js +2 -1
  52. package/dist/rules/compat/no-test-v.js +4 -2
  53. package/dist/rules/compat/no-unicode-escape.js +2 -2
  54. package/dist/rules/compat/no-wait-n.js +4 -2
  55. package/dist/rules/compat/nounset-array-proof.d.ts +8 -0
  56. package/dist/rules/compat/nounset-array-proof.js +563 -0
  57. package/dist/rules/compat/posix-absent-command.d.ts +7 -8
  58. package/dist/rules/compat/posix-absent-command.js +17 -15
  59. package/dist/rules/compat/shopt-options.js +3 -1
  60. package/dist/rules/compat/version-gates.d.ts +10 -10
  61. package/dist/rules/compat/version-gates.js +21 -13
  62. package/dist/rules/create-wrapper-rule.js +12 -7
  63. package/dist/rules/index.js +2 -0
  64. package/dist/rules/no-dynamic-invocation.d.ts +0 -8
  65. package/dist/rules/no-dynamic-invocation.js +7 -15
  66. package/dist/rules/no-eval.js +3 -1
  67. package/dist/rules/no-masked-exit-status.js +15 -8
  68. package/dist/rules/no-secret-env-reads.d.ts +4 -3
  69. package/dist/rules/no-secret-env-reads.js +37 -14
  70. package/dist/rules/no-secret-flags.d.ts +2 -1
  71. package/dist/rules/no-secret-flags.js +7 -8
  72. package/dist/rules/no-unchecked-cd.js +3 -1
  73. package/dist/rules/no-undeclared-commands.d.ts +1 -15
  74. package/dist/rules/no-undeclared-commands.js +3 -15
  75. package/dist/rules/no-unused-declarations.d.ts +1 -18
  76. package/dist/rules/no-unused-declarations.js +9 -34
  77. package/dist/rules/prefer-local.js +4 -3
  78. package/dist/rules/quote-expansions.js +3 -28
  79. package/dist/rules/require-network-wrapper.d.ts +2 -17
  80. package/dist/rules/require-network-wrapper.js +33 -4
  81. package/dist/rules/require-tty-guard.js +30 -1
  82. package/dist/rules/resolve-declaration-convention.d.ts +5 -6
  83. package/dist/rules/resolve-declaration-convention.js +5 -6
  84. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  85. package/dist/rules/resolve-network-wrapper.js +57 -26
  86. package/dist/rules/resolve-transparent-wrappers.d.ts +43 -0
  87. package/dist/rules/resolve-transparent-wrappers.js +98 -0
  88. package/dist/rules/set-options.d.ts +2 -1
  89. package/dist/rules/set-options.js +17 -5
  90. package/dist/rules/shebang-interpreter.js +4 -2
  91. package/dist/rules/transparent-wrappers.d.ts +4 -3
  92. package/dist/rules/transparent-wrappers.js +5 -4
  93. package/dist/rules/user-pattern.d.ts +5 -3
  94. package/dist/rules/user-pattern.js +6 -4
  95. package/dist/syntax/index.d.ts +4 -0
  96. package/dist/syntax/index.js +4 -0
  97. package/docs/compat.md +90 -34
  98. package/docs/custom-rule-tutorial.md +9 -3
  99. package/docs/examples/no-listed-commands-rule.ts +2 -2
  100. package/docs/rules/README.md +38 -5
  101. package/docs/rules/_template.md +2 -2
  102. package/docs/rules/compat/no-append-all-redirect.md +12 -12
  103. package/docs/rules/compat/no-associative-arrays.md +14 -14
  104. package/docs/rules/compat/no-brace-sequence-padding-step.md +90 -0
  105. package/docs/rules/compat/no-case-attributes.md +13 -13
  106. package/docs/rules/compat/no-case-fallthrough.md +12 -12
  107. package/docs/rules/compat/no-case-modification.md +11 -11
  108. package/docs/rules/compat/no-coproc.md +13 -13
  109. package/docs/rules/compat/no-declare-g.md +13 -13
  110. package/docs/rules/compat/no-double-bracket.md +2 -2
  111. package/docs/rules/compat/no-epoch-variables.md +13 -13
  112. package/docs/rules/compat/no-fd-variable.md +12 -12
  113. package/docs/rules/compat/no-globstar.md +13 -13
  114. package/docs/rules/compat/no-lastpipe-semantics.md +13 -13
  115. package/docs/rules/compat/no-let.md +2 -2
  116. package/docs/rules/compat/no-mapfile.md +16 -16
  117. package/docs/rules/compat/no-nameref.md +13 -13
  118. package/docs/rules/compat/no-negative-array-index.md +14 -14
  119. package/docs/rules/compat/no-nounset-empty-array.md +72 -17
  120. package/docs/rules/compat/no-parameter-transformation.md +19 -19
  121. package/docs/rules/compat/no-pipe-all.md +13 -13
  122. package/docs/rules/compat/no-printf-time-format.md +13 -13
  123. package/docs/rules/compat/no-source.md +2 -2
  124. package/docs/rules/compat/no-test-v.md +12 -12
  125. package/docs/rules/compat/no-unicode-escape.md +13 -13
  126. package/docs/rules/compat/no-wait-n.md +13 -13
  127. package/docs/rules/no-dynamic-invocation.md +17 -12
  128. package/docs/rules/no-eval.md +6 -1
  129. package/docs/rules/no-masked-exit-status.md +4 -0
  130. package/docs/rules/no-secret-env-reads.md +34 -3
  131. package/docs/rules/no-secret-flags.md +19 -2
  132. package/docs/rules/no-unchecked-cd.md +9 -2
  133. package/docs/rules/no-undeclared-commands.md +16 -15
  134. package/docs/rules/no-unused-declarations.md +17 -20
  135. package/docs/rules/prefer-local.md +20 -2
  136. package/docs/rules/quote-expansions.md +8 -5
  137. package/docs/rules/require-network-wrapper.md +22 -17
  138. package/docs/rules/require-shebang.md +10 -4
  139. package/docs/rules/require-strict-mode.md +6 -0
  140. package/docs/rules/require-tty-guard.md +4 -1
  141. package/docs/shellcheck.md +10 -5
  142. package/docs/transparent-wrappers.md +97 -0
  143. package/docs/untrusted-code.md +440 -0
  144. package/docs/wrapper-rules.md +9 -0
  145. package/package.json +2 -2
  146. package/dist/language/end-of-input-marker.d.ts +0 -29
  147. package/dist/language/end-of-input-marker.js +0 -88
@@ -88,9 +88,11 @@ This rule takes one options object:
88
88
  - `allow` (`string[]`, default `[]`): variable names that may be assigned in a function without
89
89
  `local`.
90
90
  - `allowPattern` (`string`, default `"^[A-Z][A-Z0-9_]*$"`): a regular expression, compiled with the
91
- `u` flag, that exempts every name it matches. A value that is not a valid regular expression is a
91
+ `u` flag (Unicode, case-sensitive), that exempts every name it matches. A value that is not a valid regular expression is a
92
92
  configuration error. Setting it replaces the default, so `allowPattern: '^g_'` stops exempting
93
- UPPER_CASE names.
93
+ UPPER_CASE names. For example, `'^g_'` exempts `g_cache` but not `G_cache`.
94
+ See [regular-expression options](./README.md#regular-expression-options) for the shared
95
+ compilation and configuration-error contract.
94
96
 
95
97
  ```js
96
98
  // eslint.config.js
@@ -104,6 +106,22 @@ This rule takes one options object:
104
106
  }
105
107
  ```
106
108
 
109
+ Regex syntax errors fail when this rule first runs on a file, naming the rule, option and
110
+ pattern. `--print-config` will not catch any regex syntax errors. See the
111
+ [shared compilation contract](./README.md#regular-expression-options).
112
+
113
+ ### Matching examples
114
+
115
+ These examples use only this rule; a finding means the assignment needs `local`.
116
+ Option values below are JSON.
117
+
118
+ | Option value | Shell input | Findings |
119
+ | --- | --- | --- |
120
+ | `{"allowPattern":"^g_"}` | `f(){ g_cache=1; }` | 0 |
121
+ | `{"allowPattern":"^g_"}` | `f(){ G_cache=1; }` | 1 |
122
+ | `{"allowPattern":"^\\p{Lu}+$"}` | `f(){ CACHE=1; }` | 0 |
123
+ | `{"allowPattern":"^\\p{Lu}+$"}` | `f(){ cache=1; }` | 1 |
124
+
107
125
  ### Patterns run against script-controlled text
108
126
 
109
127
  `allowPattern` is tested against every variable name the linted script assigns in a function, and
@@ -137,7 +137,8 @@ exec "$@" # "$@" keeps every argument intact
137
137
 
138
138
  ## When not to use it
139
139
 
140
- Disable the rule if you run shellcheck, which already reports the same problem as SC2086
140
+ Disable the rule if you run shellcheck and want to avoid double reports. This also drops
141
+ its checks in command-name position and `for … in` lists, which SC2086 does not report
141
142
  (see below). For one line that relies on splitting on purpose, such as passing `$CFLAGS` to a
142
143
  compiler, disable it on that line (`# eslint-disable-next-line sh/quote-expansions`) or rewrite
143
144
  the value as an array (`"${cflags[@]}"`).
@@ -147,11 +148,13 @@ the value as an array (`"${cflags[@]}"`).
147
148
  eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
148
149
  layer. Run shellcheck beside it.
149
150
 
150
- This rule is one of eslint-sh's two deliberate overlaps with shellcheck. It covers the same
151
+ This rule is one of eslint-sh's
152
+ [deliberate overlaps with shellcheck](../shellcheck.md#deliberate-overlaps). It covers the same
151
153
  problem as SC2086 (double quote to prevent globbing and word splitting), and SC2068 for unquoted
152
154
  `$@`. It exists for teams that run eslint-sh without shellcheck, so that this common mistake is
153
- still caught. **If you run shellcheck, disable this rule**, so the same line is not reported
154
- twice. The rule does not try to match SC2086's heuristics: shellcheck skips some expansions it
155
- can prove are safe, such as a variable it saw assigned a number, and this rule reports them
155
+ still caught. **Disabling this rule when you run shellcheck avoids double reports**, but also
156
+ drops checks in command-name position (`$cmd arg`) and `for … in` lists (`for x in $LIST`),
157
+ which SC2086 never reports. The rule does not try to match SC2086's heuristics: shellcheck skips
158
+ some expansions it can prove are safe, such as a variable it saw assigned a number, and this rule reports them
156
159
  because it does no dataflow analysis. That is why `recommended` enables it at `warn` instead of
157
160
  `error`.
@@ -62,7 +62,7 @@ It does not depend on `languageOptions`.
62
62
  ## Options
63
63
 
64
64
  - `wrapper` (`string` or `WrapperSpec` object, no default): the wrapper network commands must be
65
- run through. When it is unset, the rule uses `settings.sh.networkWrapper` (below); with
65
+ run through. When it is unset, the rule uses `settings: { sh: { networkWrapper: 'with-retry' } }` (below); with
66
66
  neither, the rule reports nothing. A string names a wrapper that takes no options
67
67
  (or one of the default transparent wrappers, such as `timeout`, whose options are known). An
68
68
  object declares the wrapper's options so the command after them is found:
@@ -85,31 +85,36 @@ It does not depend on `languageOptions`.
85
85
  }
86
86
  ```
87
87
 
88
- ### Declaring the wrapper once: `settings.sh.networkWrapper`
88
+ ### Declaring the wrapper once
89
89
 
90
- `sh/no-undeclared-commands` and `sh/no-unused-declarations` also need to know about your wrapper:
91
- in `with-retry curl x` the script runs `curl`, and only a rule that looks through `with-retry` sees
92
- that. ESLint doesn't let one rule read another rule's options, so a wrapper set only as this rule's
93
- `wrapper` option is invisible to them, and an undeclared `curl` behind it goes unreported.
94
-
95
- Set the wrapper in shared settings instead, and all three rules read it:
90
+ Set the network policy in shared settings so every wrapper-following rule sees the
91
+ same command chain, including the declaration rules, `sh/no-eval`, and
92
+ `sh/no-dynamic-invocation`:
96
93
 
97
94
  ```js
98
95
  // eslint.config.js
99
96
  {
100
97
  settings: { sh: { declarationConvention: commentPragma, networkWrapper: 'with-retry' } },
101
- rules: { 'sh/require-network-wrapper': 'error' }, // or extend sh.configs['agent-authored']
98
+ rules: { 'sh/require-network-wrapper': 'error' },
102
99
  }
103
100
  ```
104
101
 
105
- `networkWrapper` takes the same values as the `wrapper` option, a name or a `WrapperSpec` object,
106
- and the declaration rules follow a spec's options too (`with-retry -n 3 curl`). This rule's
107
- `wrapper` option, when set, takes precedence for this rule only. A malformed value stops linting
108
- with an error starting `Invalid settings.sh.networkWrapper:`.
109
-
110
- The setting names your **network** wrapper, a policy. Other wrappers that run a command, such as
111
- a `with-lock` helper, aren't network policy: list those in the declaration rules'
112
- `transparentWrappers` option.
102
+ `networkWrapper` accepts a name or a JSON-expressible `WrapperSpec` object, like the
103
+ `wrapper` rule option. A custom spec for a default wrapper name takes effect in every
104
+ consumer. Absolute and relative utility wrapper paths match by basename:
105
+ `/opt/bin/with-retry curl x` and `./bin/with-retry curl x` both run `curl` through
106
+ `with-retry`, and an undeclared `curl` is reported by the declaration rule.
107
+
108
+ A `wrapper` option that disagrees with the shared network policy now throws a clear
109
+ configuration error. Equivalent values are accepted; there is no silent override.
110
+ A wrapper set only as a rule option remains local to this network policy because other
111
+ rules cannot read its options. Prefer the shared setting. Malformed settings also stop linting.
112
+
113
+ Other project wrappers belong in
114
+ `settings: { sh: { transparentWrappers: ['with-lock'] } }`. This factory-based rule
115
+ reads those facts too, so `with-lock curl x` cannot hide an unwrapped network call.
116
+ [Shared transparent wrappers](../transparent-wrappers.md) documents the common resolver,
117
+ conflicting specs, path matching, and migration from the removed per-rule options.
113
118
 
114
119
  ## Examples
115
120
 
@@ -24,6 +24,11 @@ This rule reads line 1 of the file and reports two problems:
24
24
  `#!/usr/bin/env -S bash -e`. The rule skips `env` options, their arguments and
25
25
  `NAME=value` assignments to find the command `env` runs.
26
26
 
27
+ The space-separated long form `#!/usr/bin/env --split-string bash -eu` is reported:
28
+ GNU env receives `--split-string bash -eu` as one option name and rejects it. GNU
29
+ `--split-string=bash -eu` is accepted here, but macOS env does not support the long form;
30
+ use `-S` across both systems.
31
+
27
32
  A relative interpreter such as `#!bash` or `#!env bash` is reported, because the kernel
28
33
  resolves it against the caller's working directory, not `PATH`.
29
34
 
@@ -150,8 +155,9 @@ eslint-sh is a convention and contract linter: a policy layer, where shellcheck
150
155
  layer. Run shellcheck beside it.
151
156
 
152
157
  shellcheck's SC2148 ("Tips depend on target shell and yours is unknown") fires when a file has
153
- no shebang and no `shell=` directive. shellcheck's aim is to know which shell to analyze for,
154
- so a `# shellcheck shell=bash` directive or a `--shell` flag silences it. This rule enforces
155
- the policy that the script itself names its interpreter, which a directive or a command-line
156
- flag does not satisfy. It also autofixes the missing line from your configured dialect.
158
+ no shebang and it cannot otherwise infer the shell. A `.bash`, `.bats` or `.ksh` extension,
159
+ a `# shellcheck shell=bash` directive or a `--shell` flag supplies that information and
160
+ silences SC2148. This rule requires the script itself to name its interpreter, so it still
161
+ reports a missing shebang in those files. It also autofixes the missing line from your configured
162
+ dialect.
157
163
  shellcheck's SC1008 ("This shebang was unrecognized") overlaps `unrecognizedInterpreter`.
@@ -142,6 +142,12 @@ This rule takes one options object:
142
142
  `bash script.sh`, so a `set` line is the only prologue that applies however the script is
143
143
  started.
144
144
 
145
+ Relative interpreters such as `#!env -S bash -eu` receive no credit. Neither does
146
+ `#!/usr/bin/env --split-string bash -eu`: GNU env rejects the space-separated long
147
+ option when invoked through a kernel shebang. GNU's equals form
148
+ `#!/usr/bin/env --split-string=bash -eu` receives credit; macOS env does not support that long
149
+ option. Use an absolute env path and `-S` for both systems.
150
+
145
151
  ```js
146
152
  // eslint.config.js
147
153
  {
@@ -82,7 +82,10 @@ if [ -t 0 ]; then read name; fi
82
82
  A multi-line statement that starts its own line is wrapped on lines of its own. Anywhere else the
83
83
  wrapper goes around it in place, because an earlier command on the line may have opened a
84
84
  here-document whose body begins at the next line break; a line break goes before `fi` only when the
85
- statement ends in a here-document of its own. A statement run in the background (`read x &`)
85
+ statement ends in a here-document of its own. A statement whose here-document opener shares its
86
+ line with another command (`read x 3<<EOF; echo hi`, or a trailing comment) gets no suggestion: the
87
+ statement's source runs on to the terminator, so wrapping it would pull that command into the guard
88
+ and it would run only on a terminal. A statement run in the background (`read x &`)
86
89
  gets no suggestion, because wrapping it would change what is backgrounded. A statement whose exit
87
90
  status steers control flow (an `if`/`while`/`until` condition, or an operand of `&&` or `||`) is
88
91
  reported without a suggestion too: the wrapper would exit 0 when there is no terminal, which
@@ -32,8 +32,8 @@ For a complete GitHub Actions job, exit codes, a pre-commit hook and editor setu
32
32
  [`eslint-sh` README](https://github.com/mike-north/eslint-sh/blob/main/packages/eslint-sh/README.md#continuous-integration).
33
33
 
34
34
  Give them the same target shell, since each reads its own configuration. shellcheck takes it
35
- from the shebang or a `# shellcheck shell=…` directive; eslint-sh takes it from
36
- `languageOptions.dialect` and `languageOptions.shellVersion`. The `bash-3.2` and
35
+ from the shebang, a `# shellcheck shell=…` directive, or a `.bash`, `.bats` or `.ksh` extension;
36
+ eslint-sh takes it from `languageOptions.dialect` and `languageOptions.minBashVersion`. The `bash-3.2` and
37
37
  `posix-portable` presets set the latter for you, and
38
38
  [`sh/shebang-matches-dialect`](./rules/shebang-matches-dialect.md) reports a
39
39
  shebang that disagrees with the configured dialect, so the two tools do not end up advising for
@@ -68,19 +68,24 @@ reason given, and each rule's page has a "Relationship to shellcheck" section. T
68
68
 
69
69
  | eslint-sh rule | Overlapping shellcheck code | Why the rule exists anyway |
70
70
  | -------------- | --------------------------- | -------------------------- |
71
- | [`sh/quote-expansions`](./rules/quote-expansions.md) | SC2086, SC2068 | Makes unquoted expansions a project policy with a severity you choose, and offers a quoting suggestion. It is `warn` in `recommended`; disable it if you already enforce SC2086. |
71
+ | [`sh/quote-expansions`](./rules/quote-expansions.md) | SC2086, SC2068 | Makes unquoted expansions a project policy with a severity you choose, and offers a quoting suggestion. It is `warn` in `recommended`; disabling it when you enforce SC2086 avoids double reports, but also drops checks in command-name position and `for … in` lists, which SC2086 never reports. |
72
72
  | [`sh/no-eval`](./rules/no-eval.md) | SC2294 (`eval` on arrays), SC1090 (non-literal `source`) | shellcheck flags `eval` only in specific misuse patterns. The rule treats every `eval` and every non-literal `source` as a reviewable exception. |
73
- | [`sh/no-unchecked-cd`](./rules/no-unchecked-cd.md) | SC2164 | Kept for files that run without `set -e`; the two agree on the common cases. |
74
- | [`sh/require-shebang`](./rules/require-shebang.md) | SC2148, SC1008 | A required, recognized shebang is a rule of the repository, not a hint about the target shell. |
73
+ | [`sh/no-unchecked-cd`](./rules/no-unchecked-cd.md) | SC2164 | Offers a suggestion and covers `pushd` and `popd` too. shellcheck suppresses SC2164 for shebang errexit flags; `ignoreWhenErrexit` reads only prologue `set` statements, so the rule still reports those unchecked directory changes. |
74
+ | [`sh/require-shebang`](./rules/require-shebang.md) | SC2148, SC1008 | Requires a recognized shebang even when shellcheck infers the shell from a `.bash`, `.bats` or `.ksh` extension, a directive or a command-line flag and emits no SC2148. |
75
75
  | [`sh/no-masked-exit-status`](./rules/no-masked-exit-status.md) | SC2155 | `require-strict-mode` makes `set -euo pipefail` a contract, and `local x=$(cmd)` is the most common way a script that honours it still swallows a failure. shellcheck offers no fix; the rule offers the split as an editor suggestion and can be limited to files where errexit is on. It is not in `recommended`, because that preset runs beside shellcheck. |
76
76
  | [`sh/prefer-local`](./rules/prefer-local.md) | SC2155 (a related check) | Requires `local` declarations, and its fix declares the variable on a separate line, which avoids SC2155. |
77
77
  | [`sh/compat/no-associative-arrays`](./rules/compat/no-associative-arrays.md), [`no-case-modification`](./rules/compat/no-case-modification.md), [`no-negative-array-index`](./rules/compat/no-negative-array-index.md), [`no-mapfile`](./rules/compat/no-mapfile.md) | SC3044 (the `declare` or `mapfile` command, not the `-A` option), SC3043 (`local -A`), SC3054 (`${a[-1]}`), SC3059 (case modification) | shellcheck reports these only for scripts it treats as POSIX `sh`. These rules report them for a Bash version you name. |
78
78
  | [`sh/compat/no-double-bracket`](./rules/compat/no-double-bracket.md), [`no-let`](./rules/compat/no-let.md), [`no-source`](./rules/compat/no-source.md) | SC3010 (`[[`), SC3039 (`let`), SC3046 (`source`), under `-s sh` only | Under `-s bash` shellcheck reports nothing for `[[` or `source`, and for `let` only SC2219, a style note that prefers `(( ))`. These rules report them for the dialect you configure, because they fail on dash and would otherwise defeat a preset named for portability. |
79
79
  | [`sh/compat/no-test-v`](./rules/compat/no-test-v.md), [`no-wait-n`](./rules/compat/no-wait-n.md), [`no-globstar`](./rules/compat/no-globstar.md), [`no-pipe-all`](./rules/compat/no-pipe-all.md), [`no-fd-variable`](./rules/compat/no-fd-variable.md), [`no-unicode-escape`](./rules/compat/no-unicode-escape.md) | SC3016 (`test -v`, with SC3010 for `[[`), SC3045 (`wait -n`), SC3044 (`shopt`), SC3029 (`\|&`), SC3022 (`{fd}`), SC3003 (`$'…'`) | shellcheck reports these Bash features only for scripts it treats as POSIX `sh`. These rules report them for a Bash version you name. |
80
80
  | [`sh/compat/no-nameref`](./rules/compat/no-nameref.md), [`no-case-attributes`](./rules/compat/no-case-attributes.md) | SC3043 (`local`), SC3044 (`declare`, `typeset`): the command, not the `-n`, `-l` or `-u` option | Under `-s sh` shellcheck reports the command that carries the option; under `-s bash` it reports nothing for either. These rules report the option for a Bash version you name. |
81
+ | [`sh/compat/no-brace-sequence-padding-step`](./rules/compat/no-brace-sequence-padding-step.md) | SC3009 (brace expansion, under `-s sh` only) | Under `-s sh` shellcheck reports every brace expansion, a plain `{1..3}` and `{a,b}` too, as undefined in POSIX sh. Under `-s bash` it reports nothing, so a Bash older than 4.0 gets no finding for the padded and stepped forms. The rule reports only those two forms, for a Bash version you name. |
81
82
  | [`sh/compat/no-case-fallthrough`](./rules/compat/no-case-fallthrough.md) | SC2127 (an error, not an SC3xxx check) | Under `-s sh` shellcheck asks for a bash or ksh shebang; under `-s bash` it reports nothing. The rule reports `;&` and `;;&` for a Bash version you name. |
82
83
  | [`sh/compat/no-epoch-variables`](./rules/compat/no-epoch-variables.md) | SC3028 (`$EPOCHSECONDS`, `$EPOCHREALTIME`, under `-s sh` only) | Under `-s bash` shellcheck reports nothing, so a Bash older than 5.0 gets no finding. The rule reports them for a Bash version you name. |
83
84
  | [`sh/compat/no-append-all-redirect`](./rules/compat/no-append-all-redirect.md), [`no-parameter-transformation`](./rules/compat/no-parameter-transformation.md), [`no-printf-time-format`](./rules/compat/no-printf-time-format.md), [`no-nounset-empty-array`](./rules/compat/no-nounset-empty-array.md) | none | shellcheck reports nothing for `&>>`, `${v@op}`, `printf '%(…)T'` or an empty array under `set -u`, under `-s sh` or `-s bash` (it reports only the array syntax, SC3030 and SC3054, under `-s sh`). |
84
85
  | [`sh/compat/no-lastpipe-semantics`](./rules/compat/no-lastpipe-semantics.md) | SC2030, SC2031 | shellcheck explains the subshell effect of lastpipe; the rule reports `shopt -s lastpipe` on a Bash that lacks it. |
85
86
  | [`sh/shebang-matches-dialect`](./rules/shebang-matches-dialect.md) | SC3xxx | shellcheck judges the syntax against the shebang. This rule compares the shebang with your configured dialect, a different question. |
86
87
  | [`sh/require-strict-mode`](./rules/require-strict-mode.md) | none directly (SC2164, an unchecked `cd`, is one consequence) | shellcheck has no check that requires strict mode. |
88
+
89
+ `sh/compat/no-nounset-empty-array` requires explicit opt-in for `bash-3.2`. Enable it when a
90
+ script runs with `set -u` on Bash below 4.4 and its arrays can really be empty. Local checks
91
+ cannot prove safety across every caller or helper contract. `posix-portable` still enables it.
@@ -0,0 +1,97 @@
1
+ # Shared transparent wrappers
2
+
3
+ A transparent wrapper runs a command named in its arguments. Declare that project fact
4
+ once so every wrapper-following rule sees the same command chain:
5
+
6
+ ```js
7
+ {
8
+ settings: {
9
+ sh: {
10
+ transparentWrappers: ['with-retry', { names: ['audit-run'], argFlags: ['--ticket'] }],
11
+ },
12
+ },
13
+ }
14
+ ```
15
+
16
+ Entries are non-empty names or JSON-expressible `WrapperSpec` objects. A name preserves
17
+ the known option shape of a default wrapper such as `timeout`; an unfamiliar name
18
+ takes no flags. A spec can declare `names`, `noArgFlags`, `argFlags`,
19
+ `unresolvableFlags`, `stopsChainFlags`, `positionalOperandsBeforeCommand`,
20
+ `assignmentOperands` (`shell-identifier`, `env`, or `sudo`), and `shellBuiltin`.
21
+ `names` is required and non-empty. Regex fields and functions are not accepted.
22
+ An unknown flag stops analysis rather than guessing which operand is the command.
23
+
24
+ Project specs precede each rule's built-in table, including when a project spec names a
25
+ default wrapper. Replaced names are removed from lower-priority entries while their
26
+ unrelated aliases remain available; a builtin override cannot regain utility path semantics.
27
+ Duplicate project names and malformed entries are configuration errors.
28
+ The shared network policy, for example
29
+ `settings: { sh: { networkWrapper: 'with-retry' } }`, also supplies a transparent-wrapper
30
+ fact. Overlapping names must have the same option shape; extra transparent aliases do
31
+ not become approved network wrappers. Different option shapes for shared names are
32
+ configuration errors. A network rule's `wrapper` option must agree with both the option
33
+ shape and the full approved name set of a configured shared network policy. It cannot
34
+ override that policy silently. Omitted flag lists and empty flag lists are equivalent;
35
+ object key order and flag-list order do not matter.
36
+
37
+ Both comparisons use the published matcher's effective grammar: omitted flag lists
38
+ are empty, `shellBuiltin: false` means utility matching, and
39
+ `positionalOperandsBeforeCommand: 0` consumes no positional operand. Assignment
40
+ handling uses the selected grammar; the internal legacy alias means
41
+ `shell-identifier` only when no explicit grammar was supplied. Expanded built-in
42
+ flag patterns compare by source and flags, including presence versus absence.
43
+ This is structural grammar comparison, not arbitrary regex-language equivalence.
44
+ Explicit specs remain complete replacements: a patternless `nice` spec does not
45
+ inherit the built-in numeric-priority pattern, and conflicts with the string default
46
+ when both are configured for the same name. Internal regex fields remain forbidden
47
+ in settings; the accepted JSON fields above are unchanged.
48
+
49
+ Utility wrappers invoked by absolute or relative path match by basename:
50
+ `/opt/bin/with-retry curl x` and `./bin/with-retry curl x` both run `curl`.
51
+ An explicit path spec takes precedence over a basename fallback in either list order.
52
+ Path aliases are also built for a rule's built-in table when no shared setting is present.
53
+ The written wrapper path remains a dependency in declaration harvesting. Specs with
54
+ `shellBuiltin: true` require exact names: `/tmp/command` cannot acquire the shell
55
+ builtin's semantics. Unused-declaration completeness uses the resolved table too,
56
+ without restoring overridden built-in path semantics. These are syntactic declarations; no wrapper or corpus script is
57
+ executed or inspected to infer its behavior.
58
+
59
+ Every built-in that follows wrappers reads these facts: `no-undeclared-commands`,
60
+ `no-unused-declarations`, `no-dynamic-invocation`, `no-eval`, `no-unchecked-cd`,
61
+ `no-secret-env-reads`, `no-masked-exit-status`, and the command-based compatibility
62
+ rules. `require-network-wrapper` and rules made with `createWrapperRule` do too.
63
+ Rules retain their own built-in scopes and reporting policies. `quote-expansions`
64
+ checks expansion positions directly and does not follow wrappers.
65
+
66
+ Allowed-secret-sink exemptions apply only to argument words after the final resolved
67
+ command. In `with-retry --log "$API_TOKEN" sink`, the secret is owned by the wrapper
68
+ and is checked normally, even if `sink` is approved. In
69
+ `with-retry --log audit.log sink "$API_TOKEN"`, it is an actual sink argument and can
70
+ be exempt. The same boundary applies to default wrappers such as `env -u`.
71
+
72
+ ## Migration
73
+
74
+ Remove the `transparentWrappers` option from `no-undeclared-commands` and
75
+ `no-unused-declarations`, and remove `allowWrappers` from `no-dynamic-invocation`.
76
+ Move the list to `settings: { sh: { transparentWrappers: ['with-retry'] } }`.
77
+ Removed rule options fail ESLint schema validation; no per-rule override exists.
78
+
79
+ `allowWrappers` added checks: it made the rule follow the wrapper and report a
80
+ dynamic operand such as `with-retry "$cmd"`. Moving its list preserves that check
81
+ and makes every other wrapper-following rule see it too. To suppress a dynamic
82
+ dispatcher, use `allowInFunctions` or a justified ESLint disable comment.
83
+
84
+ Findings can change on unchanged scripts once project wrappers are configured:
85
+ wrapped `eval`, unchecked `cd`, dynamic invocations, compatibility features, and
86
+ unwrapped network commands become visible. An allowed secret sink behind a project
87
+ wrapper can become recognized, and incomplete usage behind a wrapper can prevent an
88
+ unsafe unused-declaration fix.
89
+
90
+ ## Rule packs
91
+
92
+ Call `resolveTransparentWrappers(context, builtIns)` from a rule's `create()` and
93
+ pass its result as the analysis `transparentWrappers` table. Omit `builtIns` to use
94
+ the default table. The resolver composes shared facts with that scope, validates
95
+ configuration, and adds path spellings from the linted file without mutating inputs.
96
+ Both `eslint-sh` and `@eslint-sh/eslint-plugin` export it and
97
+ `TransparentWrappersBindingContext`.