@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.
- package/README.md +19 -8
- package/dist/command-harvest/builtins/bash.d.ts +1 -1
- package/dist/command-harvest/builtins/bash.js +1 -1
- package/dist/command-harvest/builtins/bats.d.ts +2 -0
- package/dist/command-harvest/builtins/bats.js +3 -0
- package/dist/command-harvest/wrapper-table.d.ts +3 -19
- package/dist/command-harvest/wrapper-table.js +8 -38
- package/dist/configs/bash-3-2.d.ts +7 -6
- package/dist/configs/bash-3-2.js +5 -3
- package/dist/configs/posix-portable.d.ts +1 -1
- package/dist/configs/posix-portable.js +1 -0
- package/dist/declaration-conventions/read-declared-commands.d.ts +2 -2
- package/dist/declaration-conventions/read-declared-commands.js +2 -2
- package/dist/eslint-plugin-syntax.d.ts +4 -0
- package/dist/eslint-plugin.d.ts +86 -40
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/language/shell-language-options.d.ts +18 -15
- package/dist/language/shell-language-options.js +33 -26
- package/dist/language/shell-language.d.ts +10 -9
- package/dist/language/shell-language.js +18 -35
- package/dist/language/shell-source-code.d.ts +17 -9
- package/dist/language/shell-source-code.js +53 -12
- package/dist/rules/bash-variable-semantics.d.ts +22 -0
- package/dist/rules/bash-variable-semantics.js +41 -0
- package/dist/rules/compat/create-compat-rule.js +1 -1
- package/dist/rules/compat/declaration-options.d.ts +2 -2
- package/dist/rules/compat/declaration-options.js +5 -2
- package/dist/rules/compat/no-append-all-redirect.js +1 -1
- package/dist/rules/compat/no-associative-arrays.js +2 -2
- package/dist/rules/compat/no-brace-sequence-padding-step.d.ts +15 -0
- package/dist/rules/compat/no-brace-sequence-padding-step.js +287 -0
- package/dist/rules/compat/no-case-attributes.js +3 -3
- package/dist/rules/compat/no-case-fallthrough.js +2 -2
- package/dist/rules/compat/no-case-modification.js +2 -2
- package/dist/rules/compat/no-coproc.js +1 -1
- package/dist/rules/compat/no-declare-g.js +3 -3
- package/dist/rules/compat/no-double-bracket.js +2 -2
- package/dist/rules/compat/no-epoch-variables.js +2 -2
- package/dist/rules/compat/no-fd-variable.js +1 -1
- package/dist/rules/compat/no-globstar.js +1 -1
- package/dist/rules/compat/no-lastpipe-semantics.js +2 -2
- package/dist/rules/compat/no-let.js +2 -1
- package/dist/rules/compat/no-mapfile.js +4 -2
- package/dist/rules/compat/no-nameref.js +3 -3
- package/dist/rules/compat/no-negative-array-index.js +39 -9
- package/dist/rules/compat/no-nounset-empty-array.js +15 -4
- package/dist/rules/compat/no-parameter-transformation.js +2 -2
- package/dist/rules/compat/no-pipe-all.js +1 -1
- package/dist/rules/compat/no-printf-time-format.js +4 -2
- package/dist/rules/compat/no-source.js +2 -1
- package/dist/rules/compat/no-test-v.js +4 -2
- package/dist/rules/compat/no-unicode-escape.js +2 -2
- package/dist/rules/compat/no-wait-n.js +4 -2
- package/dist/rules/compat/nounset-array-proof.d.ts +8 -0
- package/dist/rules/compat/nounset-array-proof.js +563 -0
- package/dist/rules/compat/posix-absent-command.d.ts +7 -8
- package/dist/rules/compat/posix-absent-command.js +17 -15
- package/dist/rules/compat/shopt-options.js +3 -1
- package/dist/rules/compat/version-gates.d.ts +10 -10
- package/dist/rules/compat/version-gates.js +21 -13
- package/dist/rules/create-wrapper-rule.js +12 -7
- package/dist/rules/index.js +2 -0
- package/dist/rules/no-dynamic-invocation.d.ts +0 -8
- package/dist/rules/no-dynamic-invocation.js +7 -15
- package/dist/rules/no-eval.js +3 -1
- package/dist/rules/no-masked-exit-status.js +15 -8
- package/dist/rules/no-secret-env-reads.d.ts +4 -3
- package/dist/rules/no-secret-env-reads.js +37 -14
- package/dist/rules/no-secret-flags.d.ts +2 -1
- package/dist/rules/no-secret-flags.js +7 -8
- package/dist/rules/no-unchecked-cd.js +3 -1
- package/dist/rules/no-undeclared-commands.d.ts +1 -15
- package/dist/rules/no-undeclared-commands.js +3 -15
- package/dist/rules/no-unused-declarations.d.ts +1 -18
- package/dist/rules/no-unused-declarations.js +9 -34
- package/dist/rules/prefer-local.js +4 -3
- package/dist/rules/quote-expansions.js +3 -28
- package/dist/rules/require-network-wrapper.d.ts +2 -17
- package/dist/rules/require-network-wrapper.js +33 -4
- package/dist/rules/require-tty-guard.js +30 -1
- package/dist/rules/resolve-declaration-convention.d.ts +5 -6
- package/dist/rules/resolve-declaration-convention.js +5 -6
- package/dist/rules/resolve-network-wrapper.d.ts +19 -0
- package/dist/rules/resolve-network-wrapper.js +57 -26
- package/dist/rules/resolve-transparent-wrappers.d.ts +43 -0
- package/dist/rules/resolve-transparent-wrappers.js +98 -0
- package/dist/rules/set-options.d.ts +2 -1
- package/dist/rules/set-options.js +17 -5
- package/dist/rules/shebang-interpreter.js +4 -2
- package/dist/rules/transparent-wrappers.d.ts +4 -3
- package/dist/rules/transparent-wrappers.js +5 -4
- package/dist/rules/user-pattern.d.ts +5 -3
- package/dist/rules/user-pattern.js +6 -4
- package/dist/syntax/index.d.ts +4 -0
- package/dist/syntax/index.js +4 -0
- package/docs/compat.md +90 -34
- package/docs/custom-rule-tutorial.md +9 -3
- package/docs/examples/no-listed-commands-rule.ts +2 -2
- package/docs/rules/README.md +38 -5
- package/docs/rules/_template.md +2 -2
- package/docs/rules/compat/no-append-all-redirect.md +12 -12
- package/docs/rules/compat/no-associative-arrays.md +14 -14
- package/docs/rules/compat/no-brace-sequence-padding-step.md +90 -0
- package/docs/rules/compat/no-case-attributes.md +13 -13
- package/docs/rules/compat/no-case-fallthrough.md +12 -12
- package/docs/rules/compat/no-case-modification.md +11 -11
- package/docs/rules/compat/no-coproc.md +13 -13
- package/docs/rules/compat/no-declare-g.md +13 -13
- package/docs/rules/compat/no-double-bracket.md +2 -2
- package/docs/rules/compat/no-epoch-variables.md +13 -13
- package/docs/rules/compat/no-fd-variable.md +12 -12
- package/docs/rules/compat/no-globstar.md +13 -13
- package/docs/rules/compat/no-lastpipe-semantics.md +13 -13
- package/docs/rules/compat/no-let.md +2 -2
- package/docs/rules/compat/no-mapfile.md +16 -16
- package/docs/rules/compat/no-nameref.md +13 -13
- package/docs/rules/compat/no-negative-array-index.md +14 -14
- package/docs/rules/compat/no-nounset-empty-array.md +72 -17
- package/docs/rules/compat/no-parameter-transformation.md +19 -19
- package/docs/rules/compat/no-pipe-all.md +13 -13
- package/docs/rules/compat/no-printf-time-format.md +13 -13
- package/docs/rules/compat/no-source.md +2 -2
- package/docs/rules/compat/no-test-v.md +12 -12
- package/docs/rules/compat/no-unicode-escape.md +13 -13
- package/docs/rules/compat/no-wait-n.md +13 -13
- package/docs/rules/no-dynamic-invocation.md +17 -12
- package/docs/rules/no-eval.md +6 -1
- package/docs/rules/no-masked-exit-status.md +4 -0
- package/docs/rules/no-secret-env-reads.md +34 -3
- package/docs/rules/no-secret-flags.md +19 -2
- package/docs/rules/no-unchecked-cd.md +9 -2
- package/docs/rules/no-undeclared-commands.md +16 -15
- package/docs/rules/no-unused-declarations.md +17 -20
- package/docs/rules/prefer-local.md +20 -2
- package/docs/rules/quote-expansions.md +8 -5
- package/docs/rules/require-network-wrapper.md +22 -17
- package/docs/rules/require-shebang.md +10 -4
- package/docs/rules/require-strict-mode.md +6 -0
- package/docs/rules/require-tty-guard.md +4 -1
- package/docs/shellcheck.md +10 -5
- package/docs/transparent-wrappers.md +97 -0
- package/docs/untrusted-code.md +440 -0
- package/docs/wrapper-rules.md +9 -0
- package/package.json +2 -2
- package/dist/language/end-of-input-marker.d.ts +0 -29
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
-
*
|
|
71
|
-
*
|
|
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
|
|
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 -
|
|
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
|
|
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 -
|
|
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)}.`);
|
package/dist/syntax/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/syntax/index.js
CHANGED
|
@@ -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
|
-
`
|
|
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
|
|
13
|
-
you configure: Bash mode parses `declare -A` and `mapfile` even with `
|
|
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
|
-
|
|
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-
|
|
67
|
-
the version gates. The `bash-3.2` preset enables
|
|
68
|
-
|
|
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
|
|
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',
|
|
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
|
-
- **`
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
101
|
-
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
- **`dialect: "
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
112
|
-
|
|
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',
|
|
115
|
-
and enables
|
|
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-
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|