@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
@@ -0,0 +1,98 @@
1
+ import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
2
+ import { networkWrapperSpec, sameWrapperOptions, validateWrapperSetting, } from './resolve-network-wrapper.js';
3
+ /** The complete nested binding, used in configuration errors. */
4
+ const SETTING = 'settings: { sh: { transparentWrappers: [...] } }';
5
+ /** A name-only entry preserves a known utility's default flag handling. */
6
+ function asSpec(value) {
7
+ if (typeof value !== 'string') {
8
+ return value;
9
+ }
10
+ return (DEFAULT_TRANSPARENT_WRAPPERS.find((spec) => spec.names.includes(value)) ?? { names: [value] });
11
+ }
12
+ /**
13
+ * Returns the shared project wrappers followed by `builtIns` (defaults when
14
+ * omitted). Entries accept command names or JSON-expressible `WrapperSpec`s
15
+ * in `settings: { sh: { transparentWrappers: [...] } }`. The shared network
16
+ * wrapper is also transparent; conflicting specs for the same project name
17
+ * throw rather than allowing policies to disagree. Shared specs compare effective
18
+ * grammars for overlapping names, normalizing matcher defaults and retaining
19
+ * expanded built-in flag patterns; an extra transparent alias does not acquire
20
+ * network approval. Explicit specs remain complete replacements. Duplicate project
21
+ * names are rejected so list order cannot silently choose their meaning.
22
+ *
23
+ * Utility wrappers match absolute and relative paths by basename. Path words
24
+ * remain unchanged in analysis results. `shellBuiltin: true` entries match
25
+ * exact names only: an executable path cannot acquire in-process semantics.
26
+ * An explicit path spec takes precedence over a basename fallback regardless
27
+ * of configuration order.
28
+ * Aliases are built even when only `builtIns` are supplied. Overridden names
29
+ * are removed from lower-priority entries, preserving their unrelated aliases
30
+ * and preventing a utility entry from undoing an exact-name builtin override.
31
+ * The input tables are never mutated.
32
+ *
33
+ * @param context - The consuming rule's context.
34
+ * @param builtIns - The rule's own built-in wrapper scope.
35
+ * @throws Error when a configured wrapper is malformed or contradictory.
36
+ * @public
37
+ */
38
+ export function resolveTransparentWrappers(context, builtIns = DEFAULT_TRANSPARENT_WRAPPERS) {
39
+ const sh = context.settings.sh;
40
+ const value = typeof sh === 'object' && sh !== null
41
+ ? sh.transparentWrappers
42
+ : undefined;
43
+ if (value !== undefined && !Array.isArray(value)) {
44
+ throw new Error(`Invalid ${SETTING}: expected an array of command names or wrapper specs.`);
45
+ }
46
+ const configured = (value ?? []).map((entry) => asSpec(validateWrapperSetting(entry, SETTING)));
47
+ const names = new Set();
48
+ for (const spec of configured) {
49
+ for (const name of spec.names) {
50
+ if (names.has(name)) {
51
+ throw new Error(`Invalid ${SETTING}: duplicate wrapper name ${JSON.stringify(name)}.`);
52
+ }
53
+ names.add(name);
54
+ }
55
+ }
56
+ const network = networkWrapperSpec(context.settings);
57
+ for (const spec of configured) {
58
+ if (network !== null &&
59
+ spec.names.some((name) => network.names.includes(name)) &&
60
+ !sameWrapperOptions(spec, network)) {
61
+ throw new Error(`Invalid ${SETTING}: a wrapper spec conflicts with the shared networkWrapper.`);
62
+ }
63
+ }
64
+ // Precedence applies to names, including their path-matching semantics.
65
+ // Keeping a shadowed utility name could recreate an alias that a higher
66
+ // priority shellBuiltin entry explicitly excludes.
67
+ const table = [];
68
+ const claimed = new Set();
69
+ for (const spec of [...configured, ...(network === null ? [] : [network]), ...builtIns]) {
70
+ const remaining = spec.names.filter((name) => !claimed.has(name));
71
+ if (remaining.length === 0) {
72
+ continue;
73
+ }
74
+ table.push(remaining.length === spec.names.length ? spec : { ...spec, names: remaining });
75
+ for (const name of remaining) {
76
+ claimed.add(name);
77
+ }
78
+ }
79
+ // Relative utility paths need exact aliases in sh-ast. Derive them only from
80
+ // static argv words in this file; no path is resolved or executed here.
81
+ const paths = new Map();
82
+ for (const site of context.sourceCode.getCommandSites()) {
83
+ for (const word of site.argv) {
84
+ if (!word.static || !word.text.includes('/') || paths.has(word.text)) {
85
+ continue;
86
+ }
87
+ const base = word.text.slice(word.text.lastIndexOf('/') + 1);
88
+ const spec = table.find((entry) => entry.names.includes(word.text)) ??
89
+ table.find((entry) => entry.shellBuiltin !== true && entry.names.includes(base));
90
+ // Keep the selected exact spelling ahead of generic entries; deriving
91
+ // an alias from an earlier basename would erase an explicit path's flags.
92
+ if (spec !== undefined) {
93
+ paths.set(word.text, { ...spec, names: [word.text] });
94
+ }
95
+ }
96
+ }
97
+ return [...paths.values(), ...table];
98
+ }
@@ -99,7 +99,8 @@ export declare function formatSetCommand(options: readonly string[]): string;
99
99
  * (`#!/bin/bash -eu`); more than one word (`#!/bin/bash -e -u`) arrives as a
100
100
  * single argument the shell rejects, and counts as nothing.
101
101
  * - `#!/usr/bin/env -S <shell> <words…>`: `env -S` splits the rest into words,
102
- * so every word after the shell name is read.
102
+ * so every word after the shell name is read. GNU env's
103
+ * `--split-string=<shell> <words…>` is read the same way; macOS env supports only `-S`.
103
104
  *
104
105
  * Plain `#!/usr/bin/env <shell> <flags>` counts as nothing: without `-S`,
105
106
  * Linux passes `<shell> <flags>` to `env` as one command name.
@@ -168,7 +168,8 @@ export function formatSetCommand(options) {
168
168
  * (`#!/bin/bash -eu`); more than one word (`#!/bin/bash -e -u`) arrives as a
169
169
  * single argument the shell rejects, and counts as nothing.
170
170
  * - `#!/usr/bin/env -S <shell> <words…>`: `env -S` splits the rest into words,
171
- * so every word after the shell name is read.
171
+ * so every word after the shell name is read. GNU env's
172
+ * `--split-string=<shell> <words…>` is read the same way; macOS env supports only `-S`.
172
173
  *
173
174
  * Plain `#!/usr/bin/env <shell> <flags>` counts as nothing: without `-S`,
174
175
  * Linux passes `<shell> <flags>` to `env` as one command name.
@@ -184,13 +185,24 @@ export function shebangOptionChanges(text) {
184
185
  .trim()
185
186
  .split(/\s+/u);
186
187
  const [interpreter = '', ...rest] = words;
187
- const isEnv = interpreter === 'env' || interpreter.endsWith('/env');
188
+ // Relative interpreters are resolved against the caller's directory, never through PATH.
189
+ if (!interpreter.startsWith('/'))
190
+ return [];
191
+ const isEnv = interpreter.endsWith('/env');
188
192
  let flagWords;
189
193
  if (isEnv) {
190
- const [split, , ...shellArgs] = rest;
191
- if (split !== '-S' && split !== '--split-string')
194
+ const [split = '', , ...shellArgs] = rest;
195
+ // The kernel's single argument makes `--split-string bash -eu` an unknown long option.
196
+ if (split === '-S') {
197
+ flagWords = shellArgs;
198
+ }
199
+ else if (split.startsWith('--split-string=')) {
200
+ // The first word carries the command when attached; an empty value puts it next.
201
+ flagWords = rest.slice(split === '--split-string=' ? 2 : 1);
202
+ }
203
+ else {
192
204
  return [];
193
- flagWords = shellArgs;
205
+ }
194
206
  }
195
207
  else {
196
208
  if (rest.length !== 1)
@@ -34,9 +34,11 @@ function envCommand(words) {
34
34
  if (word === '--') {
35
35
  return words.slice(index + 1).find((operand) => !ENV_ASSIGNMENT.test(operand));
36
36
  }
37
- if (word === '-S' || word === '--split-string') {
37
+ // GNU env sees the whole kernel argument as an option name unless the long form uses '='.
38
+ if (word === '--split-string')
39
+ return undefined;
40
+ if (word === '-S')
38
41
  continue;
39
- }
40
42
  if (word.startsWith('--split-string=')) {
41
43
  return word.slice('--split-string='.length) || words[index + 1];
42
44
  }
@@ -11,7 +11,8 @@ import type { WrapperSpec } from 'sh-ast/analyze';
11
11
  /**
12
12
  * Bash's `builtin [shell-builtin [args …]]`: runs the named builtin, bypassing
13
13
  * any function of the same name. It takes no options of its own, so the word
14
- * after it is the command it runs.
14
+ * after it is the command it runs. Only the exact builtin name has these
15
+ * semantics; an executable path with the same basename is an ordinary program.
15
16
  *
16
17
  * `sh-ast/analyze`'s default wrapper table leaves `builtin` out, so
17
18
  * `builtin "$x"` would otherwise resolve to the static command `builtin`.
@@ -42,8 +43,8 @@ export declare const BUILTIN_WRAPPER: WrapperSpec;
42
43
  * clusters ending in `c` or `s` are listed as exact tokens. Longer clusters
43
44
  * (`-xec`) end in `'unknown-flag'`.
44
45
  *
45
- * Names match exactly, like every `sh-ast/analyze` wrapper: `/bin/bash` is not
46
- * `bash`.
46
+ * Wrapper names also match the final segment of absolute paths (`/bin/bash`).
47
+ * Relative paths require an exact table entry.
47
48
  *
48
49
  * @see https://www.gnu.org/software/bash/manual/bash.html#Invoking-Bash
49
50
  * @see https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html — POSIX `sh`
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Bash's `builtin [shell-builtin [args …]]`: runs the named builtin, bypassing
3
3
  * any function of the same name. It takes no options of its own, so the word
4
- * after it is the command it runs.
4
+ * after it is the command it runs. Only the exact builtin name has these
5
+ * semantics; an executable path with the same basename is an ordinary program.
5
6
  *
6
7
  * `sh-ast/analyze`'s default wrapper table leaves `builtin` out, so
7
8
  * `builtin "$x"` would otherwise resolve to the static command `builtin`.
@@ -9,7 +10,7 @@
9
10
  * @see https://www.gnu.org/software/bash/manual/bash.html#Bash-Builtins — `builtin`
10
11
  * @internal
11
12
  */
12
- export const BUILTIN_WRAPPER = { names: ['builtin'] };
13
+ export const BUILTIN_WRAPPER = { names: ['builtin'], shellBuiltin: true };
13
14
  /**
14
15
  * The single-letter shell options that take no operand when a shell is
15
16
  * invoked: every option letter the `set` builtin accepts, plus Bash's
@@ -67,8 +68,8 @@ const SHELL_NO_ARG_LETTERS = [
67
68
  * clusters ending in `c` or `s` are listed as exact tokens. Longer clusters
68
69
  * (`-xec`) end in `'unknown-flag'`.
69
70
  *
70
- * Names match exactly, like every `sh-ast/analyze` wrapper: `/bin/bash` is not
71
- * `bash`.
71
+ * Wrapper names also match the final segment of absolute paths (`/bin/bash`).
72
+ * Relative paths require an exact table entry.
72
73
  *
73
74
  * @see https://www.gnu.org/software/bash/manual/bash.html#Invoking-Bash
74
75
  * @see https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html — POSIX `sh`
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Compiling regular-expression sources that a rule takes from its options and
3
- * tests against text from the linted script.
3
+ * tests against text from the linted script. Every source uses Unicode (`u`)
4
+ * semantics; callers choose whether matching also ignores case.
4
5
  *
5
6
  * The configuration is trusted, but the subject is not: when the plugin lints
6
7
  * a repository it does not control, the script's author picks the names a
@@ -48,7 +49,7 @@ export declare function findNestedRepetition(source: string): NestedRepetition |
48
49
  /**
49
50
  * Compiles a regular-expression source from a rule option that will be tested
50
51
  * against script-controlled text. Throws a `TypeError` naming the rule and the
51
- * option when the source is not a valid expression, or when it nests one
52
+ * option and source when it is not a valid Unicode expression, or when it nests one
52
53
  * repetition inside another (see {@link findNestedRepetition}). ESLint reports
53
54
  * the error from the rule's `create()`, so it surfaces on the first file linted
54
55
  * with the rule ("Occurred while linting <file>"), not when the config loads.
@@ -56,7 +57,8 @@ export declare function findNestedRepetition(source: string): NestedRepetition |
56
57
  * @param rule - The rule's full name, such as `sh/prefer-local`.
57
58
  * @param option - The option as the user wrote it, such as `patterns[0]`.
58
59
  * @param source - The regular-expression source.
59
- * @param flags - The flags to compile with.
60
+ * @param flags - Rule-specific flags. Unicode semantics are always added;
61
+ * case-insensitivity is preserved without changing the subject or source.
60
62
  *
61
63
  * @internal
62
64
  */
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Compiling regular-expression sources that a rule takes from its options and
3
- * tests against text from the linted script.
3
+ * tests against text from the linted script. Every source uses Unicode (`u`)
4
+ * semantics; callers choose whether matching also ignores case.
4
5
  *
5
6
  * The configuration is trusted, but the subject is not: when the plugin lints
6
7
  * a repository it does not control, the script's author picks the names a
@@ -153,7 +154,7 @@ export function findNestedRepetition(source) {
153
154
  /**
154
155
  * Compiles a regular-expression source from a rule option that will be tested
155
156
  * against script-controlled text. Throws a `TypeError` naming the rule and the
156
- * option when the source is not a valid expression, or when it nests one
157
+ * option and source when it is not a valid Unicode expression, or when it nests one
157
158
  * repetition inside another (see {@link findNestedRepetition}). ESLint reports
158
159
  * the error from the rule's `create()`, so it surfaces on the first file linted
159
160
  * with the rule ("Occurred while linting <file>"), not when the config loads.
@@ -161,14 +162,15 @@ export function findNestedRepetition(source) {
161
162
  * @param rule - The rule's full name, such as `sh/prefer-local`.
162
163
  * @param option - The option as the user wrote it, such as `patterns[0]`.
163
164
  * @param source - The regular-expression source.
164
- * @param flags - The flags to compile with.
165
+ * @param flags - Rule-specific flags. Unicode semantics are always added;
166
+ * case-insensitivity is preserved without changing the subject or source.
165
167
  *
166
168
  * @internal
167
169
  */
168
170
  export function compileUserPattern(rule, option, source, flags) {
169
171
  let pattern;
170
172
  try {
171
- pattern = new RegExp(source, flags);
173
+ pattern = new RegExp(source, flags.includes('u') ? flags : `${flags}u`);
172
174
  }
173
175
  catch {
174
176
  throw new TypeError(`${rule}: \`${option}\` must be a valid regular expression; got ${JSON.stringify(source)}.`);
@@ -18,6 +18,10 @@
18
18
  * `@eslint-sh/eslint-plugin/syntax/analyze`. That is why these names live on their own subpath
19
19
  * and are not merged into the root.
20
20
  *
21
+ * The low-level `printSync` keeps upstream behavior, including changes to literal vertical tabs
22
+ * and form feeds. It does not apply the high-level content refusal used by `format` and
23
+ * `ShellSourceCode.getPrintedReplacement`; callers printing directly must account for that loss.
24
+ *
21
25
  * @packageDocumentation
22
26
  */
23
27
  export * from 'sh-ast';
@@ -20,6 +20,10 @@
20
20
  * `@eslint-sh/eslint-plugin/syntax/analyze`. That is why these names live on their own subpath
21
21
  * and are not merged into the root.
22
22
  *
23
+ * The low-level `printSync` keeps upstream behavior, including changes to literal vertical tabs
24
+ * and form feeds. It does not apply the high-level content refusal used by `format` and
25
+ * `ShellSourceCode.getPrintedReplacement`; callers printing directly must account for that loss.
26
+ *
23
27
  * @packageDocumentation
24
28
  */
25
29
  export * from 'sh-ast';
package/docs/compat.md CHANGED
@@ -5,14 +5,14 @@ are for scripts that must run on an older Bash, such as the Bash 3.2 that macOS
5
5
  `/bin/bash`, or on a plain POSIX `sh`.
6
6
 
7
7
  `compat/*` is reserved for version gates: rules that read the gate table below and your
8
- `shellVersion`/`dialect`. A rule that checks the file against your config without reading the
8
+ `minBashVersion`/`dialect`. A rule that checks the file against your config without reading the
9
9
  target, such as [`sh/shebang-matches-dialect`](./rules/shebang-matches-dialect.md), lives outside
10
10
  the namespace.
11
11
 
12
- The parser does not do this for you. It accepts everything its grammar knows, whatever target
13
- you configure: Bash mode parses `declare -A` and `mapfile` even with `shellVersion: "3.2"`, and
12
+ The parser does not do this for you. It accepts everything its grammar knows, whatever support requirement
13
+ you configure: Bash mode parses `declare -A` and `mapfile` even with `minBashVersion: "3.2"`, and
14
14
  POSIX mode parses `declare -A` as an ordinary command. Only the `compat/*` rules enforce the
15
- target.
15
+ support requirement.
16
16
 
17
17
  A `compat/*` finding stays reported when the code is behind a runtime version guard
18
18
  (`if (( BASH_VERSINFO[0] >= 4 )); then …`): the rules do not follow control flow. Silence a guarded
@@ -31,6 +31,7 @@ fi
31
31
  | ---- | ------- | ---- | -------- |
32
32
  | [`sh/compat/no-append-all-redirect`](./rules/compat/no-append-all-redirect.md) | `&>>` | 4.0 | no |
33
33
  | [`sh/compat/no-associative-arrays`](./rules/compat/no-associative-arrays.md) | `declare -A`, `typeset -A` | 4.0 | no |
34
+ | [`sh/compat/no-brace-sequence-padding-step`](./rules/compat/no-brace-sequence-padding-step.md) | `{01..10}`, `{1..10..2}`, `{a..e..2}` | 4.0 | no |
34
35
  | [`sh/compat/no-case-attributes`](./rules/compat/no-case-attributes.md) | `declare -l`, `declare -u` (and `typeset`, `local`) | 4.0 | no |
35
36
  | [`sh/compat/no-case-fallthrough`](./rules/compat/no-case-fallthrough.md) | `;&`, `;;&` in `case` | 4.0 | no |
36
37
  | [`sh/compat/no-case-modification`](./rules/compat/no-case-modification.md) | `${v^}`, `${v^^}`, `${v,}`, `${v,,}` | 4.0 | no |
@@ -63,12 +64,13 @@ reports each operator against its own release. `sh/compat/no-unicode-escape` nee
63
64
  points but 4.3 for U+0080 to U+00FF, because the Bash 4.2 release writes those as a single Latin-1 byte
64
65
  instead of UTF-8. `sh/compat/no-nounset-empty-array` gates a behavior, not syntax: every Bash has
65
66
  `"${a[@]}"`, but through 4.3 an empty array aborts the script under `set -u`. `every` means every Bash release has the feature, so only
66
- `dialect: "posix"` can lack it: those three are the POSIX-only gates, and the other twenty-one are
67
- the version gates. The `bash-3.2` preset enables the version gates and the `posix-portable` preset
68
- enables all twenty-four; see
67
+ `dialect: "posix"` can lack it: those three are the POSIX-only gates, and the other twenty-two are
68
+ the version gates. The `bash-3.2` preset enables twenty-one version gates, excluding the opt-in
69
+ `sh/compat/no-nounset-empty-array`, and the `posix-portable` preset
70
+ enables all twenty-five; see
69
71
  [Presets](#presets).
70
72
 
71
- ## Setting the target
73
+ ## Setting required shell support
72
74
 
73
75
  Every `compat/*` rule reads the same two `languageOptions`, and nothing else. They have no rule
74
76
  options of their own:
@@ -81,7 +83,7 @@ import sh from '@eslint-sh/eslint-plugin';
81
83
  export default defineConfig({
82
84
  files: ['**/*.sh'],
83
85
  extends: [sh.configs.recommended],
84
- languageOptions: { dialect: 'bash', shellVersion: '3.2' },
86
+ languageOptions: { dialect: 'bash', minBashVersion: '3.2' },
85
87
  rules: {
86
88
  'sh/compat/no-associative-arrays': 'error',
87
89
  'sh/compat/no-mapfile': 'error',
@@ -92,33 +94,67 @@ export default defineConfig({
92
94
 
93
95
  A gated feature is available, and its rule reports nothing, according to these rules:
94
96
 
95
- - **`shellVersion` unset means the newest version.** With no `shellVersion`, every Bash feature
96
- is available and no `compat/*` rule reports under `dialect: "bash"`. Set a version to opt in.
97
- - **Versions compare numerically, major first, then minor.** `shellVersion` has the shape
97
+ - **`minBashVersion` is a floor.** `"3.2"` means the script must support Bash 3.2 and
98
+ every newer Bash. Nothing checks for a version being too new. Unset means no Bash
99
+ version constraint, so Bash version gates report nothing under `bash` or `bats`.
100
+ - **Versions compare numerically, major first, then minor.** `minBashVersion` has the shape
98
101
  `MAJOR[.MINOR]`. A missing minor is `0`, so `"4"` is the same as `"4.0"`. Each part is a
99
102
  number, not text, so `"3.10"` is newer than `"3.2"` and still older than `"4.0"`. A feature
100
- is available when `shellVersion` is its minimum version or newer.
101
- - **`dialect: "posix"` makes Bash-only features unavailable at any `shellVersion`.** A POSIX sh
102
- script cannot rely on a feature that only Bash has, whatever version number is configured.
103
- - **`dialect: "bats"` is gated as Bash**, because Bats test files run under Bash.
104
- `shellVersion` is the version of that Bash.
105
- - **`dialect: "mksh"` and `dialect: "zsh"` are not gated.** These shells number their own
106
- releases, so a `shellVersion` there is not a Bash version, and the rules have no data on
107
- which of their releases added which feature. The `compat/*` rules report nothing for them.
103
+ is available when the configured floor is its first Bash release or newer.
104
+ - **Only `bash` and `bats` accept `minBashVersion`.** An omitted `dialect` defaults to
105
+ `bash`. Bats test files run under Bash and use the same floor. With `posix`, `mksh` or
106
+ `zsh`, remove `minBashVersion` or change the dialect to `bash` or `bats`; otherwise
107
+ ESLint rejects the config before linting.
108
+ - **`dialect: "posix"` makes Bash-only features unavailable.** A POSIX sh script cannot
109
+ rely on a feature that only Bash has.
110
+ - **`dialect: "mksh"` and `dialect: "zsh"` are not gated.** The table has no release data
111
+ for these shells, so the `compat/*` rules report nothing for them.
112
+
113
+ ### Migrating from `shellVersion`
114
+
115
+ Replace `languageOptions.shellVersion` with `languageOptions.minBashVersion` and custom
116
+ rule reads of `sourceCode.shellVersion` with `sourceCode.minBashVersion`. The old option
117
+ is removed, with no alias: ESLint rejects it with migration guidance. The normalized and
118
+ JSON option types use `minBashVersion` too. For `posix`, `mksh` and `zsh`, remove the old
119
+ option rather than renaming it, because a Bash floor does not apply to these dialects.
120
+
121
+ Check inherited options too. Extending `sh.configs['bash-3.2']` and then setting
122
+ `languageOptions: { dialect: 'posix' }` retains the preset's Bash 3.2 floor and now
123
+ throws a config error. Extending `[sh.configs['bash-3.2'], sh.configs['posix-portable']]`
124
+ also throws: the later POSIX preset changes the dialect without removing the inherited
125
+ floor. Setting `minBashVersion: undefined` does **not** clear that floor during ESLint's
126
+ config merge, so it does not fix either composition.
127
+
128
+ For scripts that must support POSIX sh, replace the Bash preset with the intended POSIX
129
+ preset, without inheriting `bash-3.2` from any config that applies to those files:
130
+
131
+ ```js
132
+ // eslint.config.js
133
+ import { defineConfig } from 'eslint/config';
134
+ import sh from '@eslint-sh/eslint-plugin';
135
+
136
+ export default defineConfig({
137
+ files: ['**/*.sh'],
138
+ extends: [sh.configs.recommended, sh.configs['posix-portable']],
139
+ });
140
+ ```
108
141
 
109
142
  ## Presets
110
143
 
111
- Two presets name a deployment target and enable every rule that enforces it, so you do not list
112
- the rules yourself:
144
+ Two presets name a deployment target and enable its checks, with the empty-array nounset check
145
+ an explicit opt-in for Bash:
113
146
 
114
- - **`sh.configs['bash-3.2']`** sets `languageOptions: { dialect: 'bash', shellVersion: '3.2' }`
115
- and enables all twenty-one version-gated `compat/*` rules at `error`. Use it for scripts that must run on the Bash
116
- 3.2 that macOS ships as `/bin/bash`.
147
+ - **`sh.configs['bash-3.2']`** sets `languageOptions: { dialect: 'bash', minBashVersion: '3.2' }`
148
+ and enables twenty-one version-gated `compat/*` rules at `error`. Use it for scripts that must run on the Bash
149
+ 3.2 that macOS ships as `/bin/bash`. It leaves `sh/compat/no-nounset-empty-array` off: enable
150
+ that rule explicitly when a script uses `set -u` and arrays that can really be empty. The rule
151
+ cannot prove safety across every caller or helper contract; see its
152
+ [configuration example](./rules/compat/no-nounset-empty-array.md#options).
117
153
  - **`sh.configs['posix-portable']`** reports the Bash-only constructs in the table above, which
118
154
  fail or read as unset on dash, but not `local`, `echo -e` or the others listed under
119
155
  [What `posix-portable` does not report](#what-posix-portable-does-not-report), so a clean run does
120
156
  not prove a script runs under every POSIX `sh`. It sets `languageOptions: { dialect: 'posix' }` and
121
- enables all twenty-four `compat/*` rules, `sh/shebang-matches-dialect`, and
157
+ enables all twenty-five `compat/*` rules, `sh/shebang-matches-dialect`, and
122
158
  `sh/require-strict-mode` configured as `set -eu`, all at `error`. Strict mode is `set -eu` and not
123
159
  `set -euo pipefail` because `pipefail` joined POSIX only in POSIX.1-2024, and older `dash` and
124
160
  BusyBox `ash` reject it, so a `pipefail` requirement would make the autofix break a working
@@ -205,10 +241,6 @@ suggestion that you apply by choice. Each rule's page says when it offers one.
205
241
 
206
242
  ## Not covered
207
243
 
208
- - **Zero-padded and stepped brace sequences** (`{01..10}`, `{1..10..2}`, Bash 4.0). Bash 3.2 drops
209
- the padding and leaves the stepped form unexpanded, with no error. The parser keeps a brace
210
- expansion as part of a word's literal text and gives it no node of its own, so a rule could find
211
- one only by matching text, which would also match literal braces. No rule reports it.
212
244
  - **Bash syntax that the POSIX parser rejects.** Under `dialect: "posix"`, `|&`, `&>>`, `;&`, `;;&`,
213
245
  `{fd}` redirections, `${v@op}` and arrays are parse errors, so a POSIX file that uses them is
214
246
  reported as unparsable, and the rules for those features never run on it. `$'…'` is read as a
@@ -226,6 +258,14 @@ is installed system-wide), and runs every probe in
226
258
  scripts/compat-matrix/run.sh
227
259
  ```
228
260
 
261
+ Builds use at most two compiler jobs and retry serially if parallel `make` fails. Bash 3.2–4.2
262
+ use `config.guess` and `config.sub` from the checksum-pinned Bash 5.3 archive so their configure
263
+ scripts recognize aarch64, including Linux containers on Apple Silicon. Only platform-detection
264
+ helpers are replaced; the shell sources retain their pinned release checksums. The script keeps
265
+ running other shells after a failure, labels the affected column `download failed`,
266
+ `checksum mismatch`, or `build failed`, and exits 1 after printing the table. Invalid arguments,
267
+ including an inaccessible `--probes` directory, exit 2.
268
+
229
269
  A probe is a short script that uses one feature, or one recommended alternative, and prints a known
230
270
  value. "yes" means the shell ran it, exited 0, and printed exactly that value; "no" means it did not.
231
271
  Each Bash column is the x.y release tarball with no upstream patches applied, pinned by SHA-256 in
@@ -236,6 +276,8 @@ the script. The `dash` column is the evidence for the POSIX sh column.
236
276
  | `associative-arrays-alt-jq` | the message's `jq -r --arg k "$key" '.[$k]' map.json` lookup (needs `jq` on PATH) | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
237
277
  | `associative-arrays-typeset` | `typeset -A m` keeps two string keys apart | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
238
278
  | `associative-arrays` | `declare -A m` keeps two string keys apart | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
279
+ | `brace-pad-step` | zero-padded `{01..03}` and stepped `{1..9..4}` brace expansion | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
280
+ | `brace-sequence-alt-loop` | the messages' counting loops, the padded one with `printf '%02d\n'` and the stepped one by `i + 4`, each written exactly as printed | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
239
281
  | `case-attributes-alt-tr` | the message's `$(printf '%s' "$v" \| tr '[:upper:]' '[:lower:]')` and its upper-case twin | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
240
282
  | `case-fallthrough-alt` | the message's rewrites: the next branch's commands repeated for `;&`, and a second `case` for `;;&` | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
241
283
  | `case-fallthrough` | `;&` runs the next branch without testing its pattern | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
@@ -283,11 +325,11 @@ the script. The `dash` column is the evidence for the POSIX sh column.
283
325
  | `nounset-empty-array-star-alt` | the suggestion's `"${a[*]-}"` under `set -u` gives one empty word for an empty array, and the joined words otherwise | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
284
326
  | `nounset-empty-array-star` | `"${a[*]}"` and unquoted `${a[@]}` of an empty array under `set -u` | no | no | no | no | no | yes | yes | yes | yes | yes | no |
285
327
  | `nounset-empty-array` | `"${a[@]}"` of an empty array under `set -u` | no | no | no | no | no | yes | yes | yes | yes | yes | no |
286
- | `param-transform-alt-case-operators` | the case messages' Bash 4.0 spellings for a target from 4.0 to 5.0: `"${v^^}"`, `"${v^}"`, `"${V,,}"` | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
328
+ | `param-transform-alt-case-operators` | the case messages' Bash 4.0 spellings when the minimum supported Bash is from 4.0 to 5.0: `"${v^^}"`, `"${v^}"`, `"${V,,}"` | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
287
329
  | `param-transform-alt-declare-p` | the `@A` message's `declare -p`, whose output re-creates the variable | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
288
330
  | `param-transform-alt-printf-b` | the `@E` message's `printf '%b'` expanding backslash escapes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
289
331
  | `param-transform-alt-printf-q` | the `@Q` message's `printf '%q'`, whose output `eval` reads back as the same value | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
290
- | `param-transform-alt-tr` | the `tr` examples the `@U`, `@u` and `@L` messages print for a target older than 4.0, written exactly as printed | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
332
+ | `param-transform-alt-tr` | the `tr` examples the `@U`, `@u` and `@L` messages print when the minimum supported Bash is older than 4.0, written exactly as printed | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
291
333
  | `param-transform-eapa` | the other Bash 4.4 operators `${v@E}`, `${v@P}`, `${v@A}`, `${v@a}` | no | no | no | no | no | yes | yes | yes | yes | yes | no |
292
334
  | `param-transform-k-lower` | `${a[@]@k}` key-value pairs as separate words | no | no | no | no | no | no | no | no | yes | yes | no |
293
335
  | `param-transform-k-upper` | `${a[@]@K}` quoted key-value pairs | no | no | no | no | no | no | no | yes | yes | yes | no |
@@ -316,7 +358,7 @@ How to read it:
316
358
  matrix because it is the release just before the 4.2 boundary of `declare -g` and `lastpipe`.
317
359
  Bash 4.0 is the release just before the 4.1 boundary of `{fd}` redirections.
318
360
  - **POSIX-only gates.** The rows for `[[`, `let` and `source`, the rule table's `every`, are "yes" on
319
- every Bash in the matrix and "no" under dash, so no `shellVersion` can change whether they report.
361
+ every Bash in the matrix and "no" under dash, so no `minBashVersion` can change whether they report.
320
362
  dash runs a script past each one without a syntax error, which is why a shell's own version check
321
363
  can guard them; see [A branch for one shell](#a-branch-for-one-shell).
322
364
  - **Parameter transformations.** `@Q`, `@E`, `@P`, `@A` and `@a` start working in 4.4; `@U`, `@u`,
@@ -331,9 +373,17 @@ How to read it:
331
373
  which a minimum-version gate cannot express, so the rule does not report them.
332
374
  - **Negative subscripts.** Bash 4.2 reads `${a[-1]}`, but rejects `${#a[-1]}`, `a[-1]=Z` and
333
375
  `unset 'a[-1]'` with `bad array subscript`. 4.3 is the first release where every form works.
376
+ - **Brace sequences.** `brace-pad-step` is "no" on Bash 3.2, which prints `1 2 3 {1..9..4}` with
377
+ no error, and on dash, which prints `{01..03} {1..9..4}`; it is "yes" from 4.0. That row records
378
+ the `run.sh` result reported in issue #237; the probe was also re-run on `/bin/bash` (3.2.57),
379
+ Homebrew's Bash 5.3.9 and the system `dash`, with the same outcomes. The
380
+ `brace-sequence-alt-loop` row has not been run through `run.sh`: its probe was run only on macOS's
381
+ `/bin/bash` (3.2.57), Homebrew's Bash 5.3.9 and the system `dash`, which are distribution builds,
382
+ not the tarball releases in the other columns, and each printed the expected line. Fill the row by
383
+ running `scripts/compat-matrix/run.sh`.
334
384
  - **Alternatives.** Every alternative a message recommends for a Bash target works on Bash 3.2,
335
385
  except `param-transform-alt-case-operators` (`"${v^^}"` and its twins), which the
336
- `no-parameter-transformation` message offers only for a target from 4.0 to 5.0, where it works.
386
+ `no-parameter-transformation` message offers only when the minimum supported Bash is from 4.0 to 5.0, where it works.
337
387
  The ones that are "no" under dash (`< <(cmd)`, the `mapfile` suggestion's array loop, the
338
388
  `${arr[$((${#arr[@]}-1))]}` count, `printf -v`, `printf '%q'`, `declare -p`, the `${a[@]+…}` and
339
389
  `${a[*]-}` guards, `$'\303\251'`) use Bash syntax and appear only in Bash-target messages; the
@@ -344,4 +394,10 @@ How to read it:
344
394
  `date`. `%s` is not in POSIX `date`; the message says so.
345
395
  - **`jq`.** The `no-associative-arrays` advice runs an external program, so its row shows that the
346
396
  command as written works wherever `jq` is installed, not that a shell provides it.
397
+ - **Plain assignments.** `declare-g-alt-assign` confirms that `name=value` and `export name=value`
398
+ inside a function leave values visible after the function returns when no caller has a
399
+ same-named local. It is not a discriminating test of `declare -g`: ordinary assignment follows
400
+ a caller's local scope, whereas `declare -g` explicitly targets the global in Bash 4.3–5.3 but
401
+ assigns a same-named caller local in Bash 4.2, leaving the global unset. The row does not
402
+ establish that the plain-assignment advice is interchangeable with `declare -g` in that case.
347
403
 
@@ -421,6 +421,11 @@ with, so you don't install `sh-ast` and its version always matches the records `
421
421
  They track the plugin's pinned `sh-ast` minor: an `sh-ast` 0.x minor bump is a minor release of the
422
422
  plugin.
423
423
 
424
+ The low-level `/syntax` export `printSync` retains upstream behavior: literal vertical tabs and
425
+ form feeds can change during printing, including inside quotes. It does not apply the high-level
426
+ content refusal used by `format` or `getPrintedReplacement`. For an ESLint printed fix, use
427
+ `getPrintedReplacement`, which refuses these values with `TypeError`.
428
+
424
429
  ## 8. Printed fixes
425
430
 
426
431
  Some fixes are easier to describe as an edit to the syntax than as new text. Turning
@@ -437,7 +442,8 @@ into an ESLint fix:
437
442
  returns `{ range, text }`, ready for `fixer.replaceTextRange(range, text)`. The range is
438
443
  `node.range`, or the whole file when `edited` is a `File`. `edited` must be a root `printSync`
439
444
  accepts (`File`, `Stmt`, `Word`, `Assign`, a `Command` or a `WordPart`), and output that is not
440
- valid UTF-8 throws a `TypeError` instead of becoming a fix.
445
+ valid UTF-8 or edited syntax containing literal vertical tabs or form feeds throws a `TypeError`
446
+ instead of becoming a fix. A `File`'s `Name` is unprinted metadata and does not trigger that refusal.
441
447
 
442
448
  `docs/examples/no-backticks-rule.mjs` reports backtick command substitution and fixes it to `$(…)`:
443
449
 
@@ -714,13 +720,13 @@ import type { ShellCommandSite, ShellRuleContext } from '@eslint-sh/eslint-plugi
714
720
  /** The rule's one options object; `commands` defaults to `[]`. */
715
721
  type Options = [{ commands?: string[] }?];
716
722
 
717
- /** Reports `site` when the command it runs, after wrappers, is in `listed`. */
723
+ /** Reports the program named by `site`, after wrappers, including interpreters running strings. */
718
724
  function reportIfListed(
719
725
  context: ShellRuleContext<Options, 'listed'>,
720
726
  site: ShellCommandSite,
721
727
  listed: ReadonlySet<string>,
722
728
  ): void {
723
- const { effective } = site.resolveInvokedCommand();
729
+ const { effective } = site.resolveInvokedCommand({ embeddedCommandInterpreters: [] });
724
730
  if (effective.static && listed.has(effective.text)) {
725
731
  context.report({ node: site.node, messageId: 'listed', data: { name: effective.text } });
726
732
  }
@@ -19,13 +19,13 @@ import type { ShellCommandSite, ShellRuleContext } from '@eslint-sh/eslint-plugi
19
19
  /** The rule's one options object; `commands` defaults to `[]`. */
20
20
  type Options = [{ commands?: string[] }?];
21
21
 
22
- /** Reports `site` when the command it runs, after wrappers, is in `listed`. */
22
+ /** Reports the program named by `site`, after wrappers, including interpreters running strings. */
23
23
  function reportIfListed(
24
24
  context: ShellRuleContext<Options, 'listed'>,
25
25
  site: ShellCommandSite,
26
26
  listed: ReadonlySet<string>,
27
27
  ): void {
28
- const { effective } = site.resolveInvokedCommand();
28
+ const { effective } = site.resolveInvokedCommand({ embeddedCommandInterpreters: [] });
29
29
  if (effective.static && listed.has(effective.text)) {
30
30
  context.report({ node: site.node, messageId: 'listed', data: { name: effective.text } });
31
31
  }