@eslint-sh/eslint-plugin 0.7.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 (141) hide show
  1. package/README.md +14 -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 +4 -3
  10. package/dist/configs/posix-portable.d.ts +1 -1
  11. package/dist/declaration-conventions/read-declared-commands.d.ts +2 -2
  12. package/dist/declaration-conventions/read-declared-commands.js +2 -2
  13. package/dist/eslint-plugin-syntax.d.ts +4 -0
  14. package/dist/eslint-plugin.d.ts +86 -40
  15. package/dist/index.d.ts +5 -2
  16. package/dist/index.js +4 -2
  17. package/dist/language/shell-language-options.d.ts +18 -15
  18. package/dist/language/shell-language-options.js +33 -26
  19. package/dist/language/shell-language.d.ts +10 -9
  20. package/dist/language/shell-language.js +18 -35
  21. package/dist/language/shell-source-code.d.ts +17 -9
  22. package/dist/language/shell-source-code.js +53 -12
  23. package/dist/rules/bash-variable-semantics.d.ts +22 -0
  24. package/dist/rules/bash-variable-semantics.js +41 -0
  25. package/dist/rules/compat/create-compat-rule.js +1 -1
  26. package/dist/rules/compat/declaration-options.d.ts +2 -2
  27. package/dist/rules/compat/declaration-options.js +5 -2
  28. package/dist/rules/compat/no-append-all-redirect.js +1 -1
  29. package/dist/rules/compat/no-associative-arrays.js +2 -2
  30. package/dist/rules/compat/no-brace-sequence-padding-step.js +4 -4
  31. package/dist/rules/compat/no-case-attributes.js +3 -3
  32. package/dist/rules/compat/no-case-fallthrough.js +2 -2
  33. package/dist/rules/compat/no-case-modification.js +2 -2
  34. package/dist/rules/compat/no-coproc.js +1 -1
  35. package/dist/rules/compat/no-declare-g.js +3 -3
  36. package/dist/rules/compat/no-double-bracket.js +2 -2
  37. package/dist/rules/compat/no-epoch-variables.js +2 -2
  38. package/dist/rules/compat/no-fd-variable.js +1 -1
  39. package/dist/rules/compat/no-globstar.js +1 -1
  40. package/dist/rules/compat/no-lastpipe-semantics.js +2 -2
  41. package/dist/rules/compat/no-let.js +2 -1
  42. package/dist/rules/compat/no-mapfile.js +4 -2
  43. package/dist/rules/compat/no-nameref.js +3 -3
  44. package/dist/rules/compat/no-negative-array-index.js +39 -9
  45. package/dist/rules/compat/no-nounset-empty-array.js +15 -4
  46. package/dist/rules/compat/no-parameter-transformation.js +2 -2
  47. package/dist/rules/compat/no-pipe-all.js +1 -1
  48. package/dist/rules/compat/no-printf-time-format.js +4 -2
  49. package/dist/rules/compat/no-source.js +2 -1
  50. package/dist/rules/compat/no-test-v.js +4 -2
  51. package/dist/rules/compat/no-unicode-escape.js +2 -2
  52. package/dist/rules/compat/no-wait-n.js +4 -2
  53. package/dist/rules/compat/nounset-array-proof.d.ts +8 -0
  54. package/dist/rules/compat/nounset-array-proof.js +563 -0
  55. package/dist/rules/compat/posix-absent-command.d.ts +7 -8
  56. package/dist/rules/compat/posix-absent-command.js +17 -15
  57. package/dist/rules/compat/shopt-options.js +3 -1
  58. package/dist/rules/compat/version-gates.d.ts +9 -9
  59. package/dist/rules/compat/version-gates.js +13 -13
  60. package/dist/rules/create-wrapper-rule.js +12 -7
  61. package/dist/rules/no-dynamic-invocation.d.ts +0 -8
  62. package/dist/rules/no-dynamic-invocation.js +7 -15
  63. package/dist/rules/no-eval.js +3 -1
  64. package/dist/rules/no-masked-exit-status.js +15 -8
  65. package/dist/rules/no-secret-env-reads.d.ts +4 -3
  66. package/dist/rules/no-secret-env-reads.js +37 -14
  67. package/dist/rules/no-secret-flags.d.ts +2 -1
  68. package/dist/rules/no-secret-flags.js +7 -8
  69. package/dist/rules/no-unchecked-cd.js +3 -1
  70. package/dist/rules/no-undeclared-commands.d.ts +1 -15
  71. package/dist/rules/no-undeclared-commands.js +3 -15
  72. package/dist/rules/no-unused-declarations.d.ts +1 -18
  73. package/dist/rules/no-unused-declarations.js +9 -34
  74. package/dist/rules/prefer-local.js +4 -3
  75. package/dist/rules/quote-expansions.js +3 -28
  76. package/dist/rules/require-network-wrapper.d.ts +2 -17
  77. package/dist/rules/require-network-wrapper.js +33 -4
  78. package/dist/rules/resolve-declaration-convention.d.ts +5 -6
  79. package/dist/rules/resolve-declaration-convention.js +5 -6
  80. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  81. package/dist/rules/resolve-network-wrapper.js +57 -26
  82. package/dist/rules/resolve-transparent-wrappers.d.ts +43 -0
  83. package/dist/rules/resolve-transparent-wrappers.js +98 -0
  84. package/dist/rules/set-options.d.ts +2 -1
  85. package/dist/rules/set-options.js +17 -5
  86. package/dist/rules/shebang-interpreter.js +4 -2
  87. package/dist/rules/transparent-wrappers.d.ts +4 -3
  88. package/dist/rules/transparent-wrappers.js +5 -4
  89. package/dist/rules/user-pattern.d.ts +5 -3
  90. package/dist/rules/user-pattern.js +6 -4
  91. package/dist/syntax/index.d.ts +4 -0
  92. package/dist/syntax/index.js +4 -0
  93. package/docs/compat.md +76 -27
  94. package/docs/custom-rule-tutorial.md +9 -3
  95. package/docs/examples/no-listed-commands-rule.ts +2 -2
  96. package/docs/rules/README.md +36 -4
  97. package/docs/rules/_template.md +2 -2
  98. package/docs/rules/compat/no-append-all-redirect.md +12 -12
  99. package/docs/rules/compat/no-associative-arrays.md +14 -14
  100. package/docs/rules/compat/no-brace-sequence-padding-step.md +12 -12
  101. package/docs/rules/compat/no-case-attributes.md +13 -13
  102. package/docs/rules/compat/no-case-fallthrough.md +12 -12
  103. package/docs/rules/compat/no-case-modification.md +11 -11
  104. package/docs/rules/compat/no-coproc.md +13 -13
  105. package/docs/rules/compat/no-declare-g.md +13 -13
  106. package/docs/rules/compat/no-double-bracket.md +2 -2
  107. package/docs/rules/compat/no-epoch-variables.md +13 -13
  108. package/docs/rules/compat/no-fd-variable.md +12 -12
  109. package/docs/rules/compat/no-globstar.md +13 -13
  110. package/docs/rules/compat/no-lastpipe-semantics.md +13 -13
  111. package/docs/rules/compat/no-let.md +2 -2
  112. package/docs/rules/compat/no-mapfile.md +16 -16
  113. package/docs/rules/compat/no-nameref.md +13 -13
  114. package/docs/rules/compat/no-negative-array-index.md +14 -14
  115. package/docs/rules/compat/no-nounset-empty-array.md +72 -17
  116. package/docs/rules/compat/no-parameter-transformation.md +19 -19
  117. package/docs/rules/compat/no-pipe-all.md +13 -13
  118. package/docs/rules/compat/no-printf-time-format.md +13 -13
  119. package/docs/rules/compat/no-source.md +2 -2
  120. package/docs/rules/compat/no-test-v.md +12 -12
  121. package/docs/rules/compat/no-unicode-escape.md +13 -13
  122. package/docs/rules/compat/no-wait-n.md +13 -13
  123. package/docs/rules/no-dynamic-invocation.md +17 -12
  124. package/docs/rules/no-eval.md +6 -1
  125. package/docs/rules/no-masked-exit-status.md +4 -0
  126. package/docs/rules/no-secret-env-reads.md +34 -3
  127. package/docs/rules/no-secret-flags.md +19 -2
  128. package/docs/rules/no-unchecked-cd.md +9 -2
  129. package/docs/rules/no-undeclared-commands.md +16 -15
  130. package/docs/rules/no-unused-declarations.md +17 -20
  131. package/docs/rules/prefer-local.md +20 -2
  132. package/docs/rules/quote-expansions.md +8 -5
  133. package/docs/rules/require-network-wrapper.md +22 -17
  134. package/docs/rules/require-shebang.md +10 -4
  135. package/docs/rules/require-strict-mode.md +6 -0
  136. package/docs/shellcheck.md +9 -5
  137. package/docs/transparent-wrappers.md +97 -0
  138. package/docs/wrapper-rules.md +9 -0
  139. package/package.json +2 -2
  140. package/dist/language/end-of-input-marker.d.ts +0 -29
  141. package/dist/language/end-of-input-marker.js +0 -88
@@ -1,6 +1,6 @@
1
1
  # `sh/compat/no-globstar`
2
2
 
3
- Disallow `shopt -s globstar` in scripts whose target shell lacks the option: Bash older than 4.0,
3
+ Disallow `shopt -s globstar` in scripts that must support a shell lacking the option: Bash older than 4.0,
4
4
  including the Bash 3.2 that macOS ships as `/bin/bash`, and POSIX sh.
5
5
 
6
6
  | Configs | Fixable | Languages |
@@ -18,19 +18,19 @@ The portable alternative is `find`, for example `find dir -name '*.txt'`.
18
18
  This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
19
19
  described in [version compatibility](../../compat.md):
20
20
 
21
- - `shellVersion` unset: never reports. An unset version means the newest Bash.
22
- - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.0`.
23
- - `dialect: "posix"`: reports at any `shellVersion`, because POSIX sh has no `shopt`.
21
+ - `minBashVersion` unset: no Bash version constraint, so no report under `bash` or `bats`.
22
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `minBashVersion` is older than `4.0`.
23
+ - `dialect: "posix"`: reports regardless of Bash releases, because POSIX sh has no `shopt`.
24
24
  - `dialect: "mksh"` or `"zsh"`: never reports.
25
25
 
26
26
  ## Options
27
27
 
28
- This rule has no options. Set the target through `languageOptions`:
28
+ This rule has no options. Set the required support through `languageOptions`:
29
29
 
30
30
  ```js
31
31
  // eslint.config.js
32
32
  {
33
- languageOptions: { dialect: 'bash', shellVersion: '3.2' },
33
+ languageOptions: { dialect: 'bash', minBashVersion: '3.2' },
34
34
  rules: {
35
35
  'sh/compat/no-globstar': 'error',
36
36
  },
@@ -41,9 +41,9 @@ This rule has no options. Set the target through `languageOptions`:
41
41
 
42
42
  ### Incorrect
43
43
 
44
- With `shellVersion: "3.2"`:
44
+ With `minBashVersion: "3.2"`:
45
45
 
46
- <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 1} -->
46
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2", "findings": 1} -->
47
47
  ```sh
48
48
  shopt -s nullglob globstar
49
49
  for f in src/**/*.sh; do lint "$f"; done
@@ -51,19 +51,19 @@ for f in src/**/*.sh; do lint "$f"; done
51
51
 
52
52
  ### Correct
53
53
 
54
- With `shellVersion: "3.2"`:
54
+ With `minBashVersion: "3.2"`:
55
55
 
56
- <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
56
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2"} -->
57
57
  ```sh
58
58
  find src -name '*.sh' -exec lint {} \;
59
59
  shopt -s nullglob
60
60
  ```
61
61
 
62
- With `shellVersion: "4.0"` or newer, or with no `shellVersion`, `globstar` is fine.
62
+ With `minBashVersion: "4.0"` or newer, or with no `minBashVersion`, `globstar` is fine.
63
63
 
64
64
  ## When not to use it
65
65
 
66
- Turn the rule off when your scripts never run on Bash older than 4.0, or leave `shellVersion`
66
+ Turn the rule off when your scripts never run on Bash older than 4.0, or leave `minBashVersion`
67
67
  unset, which makes the rule report nothing.
68
68
 
69
69
  ## Relationship to shellcheck
@@ -71,4 +71,4 @@ unset, which makes the rule report nothing.
71
71
  eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
72
72
  layer. Run shellcheck beside it.
73
73
 
74
- shellcheck reports `shopt` only for a script it treats as POSIX `sh` (its SC3xxx portability checks). It does not know which Bash release a script must run on, so it cannot report `globstar` for a Bash 3.2 target. This rule exists for that target.
74
+ shellcheck reports `shopt` only for a script it treats as POSIX `sh` (its SC3xxx portability checks). It does not know which Bash release a script must run on, so it cannot report `globstar` for a script that must support Bash 3.2. This rule checks that support requirement.
@@ -1,6 +1,6 @@
1
1
  # `sh/compat/no-lastpipe-semantics`
2
2
 
3
- Disallow `shopt -s lastpipe` in scripts whose target shell lacks the option: Bash older than 4.2,
3
+ Disallow `shopt -s lastpipe` in scripts that must support a shell lacking the option: Bash older than 4.2,
4
4
  including the Bash 3.2 that macOS ships as `/bin/bash`, and POSIX sh.
5
5
 
6
6
  | Configs | Fixable | Languages |
@@ -18,19 +18,19 @@ The portable alternative is process substitution: `while read -r line; do …; d
18
18
  This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
19
19
  described in [version compatibility](../../compat.md):
20
20
 
21
- - `shellVersion` unset: never reports. An unset version means the newest Bash.
22
- - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.2`.
23
- - `dialect: "posix"`: reports at any `shellVersion`.
21
+ - `minBashVersion` unset: no Bash version constraint, so no report under `bash` or `bats`.
22
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `minBashVersion` is older than `4.2`.
23
+ - `dialect: "posix"`: reports regardless of Bash releases.
24
24
  - `dialect: "mksh"` or `"zsh"`: never reports.
25
25
 
26
26
  ## Options
27
27
 
28
- This rule has no options. Set the target through `languageOptions`:
28
+ This rule has no options. Set the required support through `languageOptions`:
29
29
 
30
30
  ```js
31
31
  // eslint.config.js
32
32
  {
33
- languageOptions: { dialect: 'bash', shellVersion: '3.2' },
33
+ languageOptions: { dialect: 'bash', minBashVersion: '3.2' },
34
34
  rules: {
35
35
  'sh/compat/no-lastpipe-semantics': 'error',
36
36
  },
@@ -41,9 +41,9 @@ This rule has no options. Set the target through `languageOptions`:
41
41
 
42
42
  ### Incorrect
43
43
 
44
- With `shellVersion: "3.2"`:
44
+ With `minBashVersion: "3.2"`:
45
45
 
46
- <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 1} -->
46
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2", "findings": 1} -->
47
47
  ```sh
48
48
  shopt -s lastpipe
49
49
  cmd | while read -r line; do total=$line; done
@@ -51,23 +51,23 @@ cmd | while read -r line; do total=$line; done
51
51
 
52
52
  ### Correct
53
53
 
54
- With `shellVersion: "3.2"`:
54
+ With `minBashVersion: "3.2"`:
55
55
 
56
- <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
56
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2"} -->
57
57
  ```sh
58
58
  while read -r line; do total=$line; done < <(cmd)
59
59
  ```
60
60
 
61
- With `shellVersion: "4.2"` or newer, or with no `shellVersion`, the original forms are fine.
61
+ With `minBashVersion: "4.2"` or newer, or with no `minBashVersion`, the original forms are fine.
62
62
 
63
63
  ## When not to use it
64
64
 
65
65
  Turn the rule off when your scripts never run on Bash older than 4.2, or leave
66
- `shellVersion` unset, which makes the rule report nothing.
66
+ `minBashVersion` unset, which makes the rule report nothing.
67
67
 
68
68
  ## Relationship to shellcheck
69
69
 
70
70
  eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
71
71
  layer. Run shellcheck beside it.
72
72
 
73
- shellcheck warns that variables assigned in the last pipeline element are lost (SC2030/SC2031), and understands `shopt -s lastpipe` for that purpose, but it cannot target a Bash version, so it cannot report the option for a Bash 3.2 target. This rule exists for that target.
73
+ shellcheck warns that variables assigned in the last pipeline element are lost (SC2030/SC2031), and understands `shopt -s lastpipe` for that purpose, but it cannot check support for a minimum Bash version, so it cannot report the option for a script that must support Bash 3.2. This rule checks that support requirement.
@@ -27,12 +27,12 @@ The portable alternative is arithmetic expansion, `n=$((1 + 2))`. The rule offer
27
27
 
28
28
  This is a POSIX-only gate. Every Bash has `let`, so whether the rule reports depends only on `dialect`, as described in [version compatibility](../../compat.md):
29
29
 
30
- - `dialect: "posix"`: reports at any `shellVersion`.
30
+ - `dialect: "posix"`: reports regardless of Bash releases.
31
31
  - `dialect: "bash"` (the default), `"bats"`, `"mksh"` or `"zsh"`: never reports.
32
32
 
33
33
  ## Options
34
34
 
35
- This rule has no options. Set the target through `languageOptions`:
35
+ This rule has no options. Set the required support through `languageOptions`:
36
36
 
37
37
  ```js
38
38
  // eslint.config.js
@@ -1,6 +1,6 @@
1
1
  # `sh/compat/no-mapfile`
2
2
 
3
- Disallow the `mapfile` builtin and its synonym `readarray` in scripts whose target shell lacks
3
+ Disallow the `mapfile` builtin and its synonym `readarray` in scripts that must support a shell lacking
4
4
  them: Bash older than 4.0, including the Bash 3.2 that macOS ships as `/bin/bash`, and POSIX sh.
5
5
 
6
6
  | Configs | Fixable | Languages |
@@ -16,9 +16,9 @@ never filled.
16
16
  This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
17
17
  described in [version compatibility](../../compat.md):
18
18
 
19
- - `shellVersion` unset: never reports. An unset version means the newest Bash.
20
- - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.0`.
21
- - `dialect: "posix"`: reports at any `shellVersion`.
19
+ - `minBashVersion` unset: no Bash version constraint, so no report under `bash` or `bats`.
20
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `minBashVersion` is older than `4.0`.
21
+ - `dialect: "posix"`: reports regardless of Bash releases.
22
22
  - `dialect: "mksh"` or `"zsh"`: never reports.
23
23
 
24
24
  The rule reports every command whose effective command is `mapfile` or `readarray`, after
@@ -31,7 +31,7 @@ For `mapfile -t NAME` or `readarray -t NAME`, written directly (no wrapper and n
31
31
  `VAR=value`) in a Bash or Bats file, the rule suggests a `while IFS= read -r` loop that fills
32
32
  the same array the same way:
33
33
 
34
- <!-- transform: {"kind": "suggestion", "dialect": "bash", "shellVersion": "3.2"} -->
34
+ <!-- transform: {"kind": "suggestion", "dialect": "bash", "minBashVersion": "3.2"} -->
35
35
 
36
36
  ```sh
37
37
  mapfile -t lines < in.txt
@@ -57,12 +57,12 @@ assignment, a wrapper, and every file under `dialect: "posix"`.
57
57
 
58
58
  ## Options
59
59
 
60
- This rule has no options. Set the target through `languageOptions`:
60
+ This rule has no options. Set the required support through `languageOptions`:
61
61
 
62
62
  ```js
63
63
  // eslint.config.js
64
64
  {
65
- languageOptions: { dialect: 'bash', shellVersion: '3.2' },
65
+ languageOptions: { dialect: 'bash', minBashVersion: '3.2' },
66
66
  rules: {
67
67
  'sh/compat/no-mapfile': 'error',
68
68
  },
@@ -73,9 +73,9 @@ This rule has no options. Set the target through `languageOptions`:
73
73
 
74
74
  ### Incorrect
75
75
 
76
- With `shellVersion: "3.2"`:
76
+ With `minBashVersion: "3.2"`:
77
77
 
78
- <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 3} -->
78
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2", "findings": 3} -->
79
79
  ```sh
80
80
  mapfile -t lines < in.txt # mapfile needs Bash 4.0
81
81
  git ls-files | readarray -t files # readarray is the same builtin
@@ -84,9 +84,9 @@ command mapfile -t lines < in.txt # reached through the command wrapper
84
84
 
85
85
  ### Correct
86
86
 
87
- With `shellVersion: "3.2"`:
87
+ With `minBashVersion: "3.2"`:
88
88
 
89
- <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
89
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2"} -->
90
90
  ```sh
91
91
  { lines=(); while IFS= read -r || [ -n "$REPLY" ]; do lines+=("$REPLY"); done; } < in.txt
92
92
 
@@ -95,16 +95,16 @@ while IFS= read -r line; do
95
95
  done < in.txt
96
96
  ```
97
97
 
98
- With `shellVersion: "4.0"` or newer, or with no `shellVersion`:
98
+ With `minBashVersion: "4.0"` or newer, or with no `minBashVersion`:
99
99
 
100
- <!-- example: {"dialect": "bash", "shellVersion": "4.0"} -->
100
+ <!-- example: {"dialect": "bash", "minBashVersion": "4.0"} -->
101
101
  ```sh
102
102
  mapfile -t lines < in.txt
103
103
  ```
104
104
 
105
105
  ## When not to use it
106
106
 
107
- Turn the rule off when your scripts never run on Bash older than 4.0, or leave `shellVersion`
107
+ Turn the rule off when your scripts never run on Bash older than 4.0, or leave `minBashVersion`
108
108
  unset, which makes the rule report nothing.
109
109
 
110
110
  ## Relationship to shellcheck
@@ -113,6 +113,6 @@ eslint-sh is a convention and contract linter: a policy layer, where shellcheck
113
113
  layer. Run shellcheck beside it.
114
114
 
115
115
  shellcheck reports Bash-only features in scripts it treats as POSIX sh (the SC3xxx checks), but
116
- it cannot target a Bash version, so it cannot report `mapfile` in a script that must run on
116
+ it cannot check support for a minimum Bash version, so it cannot report `mapfile` in a script that must run on
117
117
  Bash 3.2. shellcheck 0.11.0 does recommend `mapfile` or `read -a` over `a=($(cmd))` splitting (SC2207) under `-s bash`, which is
118
- the opposite advice for a Bash 3.2 target. This rule exists for that target.
118
+ the opposite advice for a script that must support Bash 3.2. This rule checks that support requirement.
@@ -1,6 +1,6 @@
1
1
  # `sh/compat/no-nameref`
2
2
 
3
- Disallow namerefs (`declare -n`, `typeset -n`, `local -n`) in scripts whose target shell lacks
3
+ Disallow namerefs (`declare -n`, `typeset -n`, `local -n`) in scripts that must support a shell lacking
4
4
  them: Bash older than 4.3, including the Bash 3.2 that macOS ships as `/bin/bash`, and POSIX sh.
5
5
 
6
6
  | Configs | Fixable | Languages |
@@ -18,19 +18,19 @@ The portable alternative is to pass the variable's name and use it directly. On
18
18
  This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
19
19
  described in [version compatibility](../../compat.md):
20
20
 
21
- - `shellVersion` unset: never reports. An unset version means the newest Bash.
22
- - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.3`.
23
- - `dialect: "posix"`: reports at any `shellVersion`, with the `eval` advice.
21
+ - `minBashVersion` unset: no Bash version constraint, so no report under `bash` or `bats`.
22
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `minBashVersion` is older than `4.3`.
23
+ - `dialect: "posix"`: reports regardless of Bash releases, with the `eval` advice.
24
24
  - `dialect: "mksh"` or `"zsh"`: never reports.
25
25
 
26
26
  ## Options
27
27
 
28
- This rule has no options. Set the target through `languageOptions`:
28
+ This rule has no options. Set the required support through `languageOptions`:
29
29
 
30
30
  ```js
31
31
  // eslint.config.js
32
32
  {
33
- languageOptions: { dialect: 'bash', shellVersion: '3.2' },
33
+ languageOptions: { dialect: 'bash', minBashVersion: '3.2' },
34
34
  rules: {
35
35
  'sh/compat/no-nameref': 'error',
36
36
  },
@@ -41,9 +41,9 @@ This rule has no options. Set the target through `languageOptions`:
41
41
 
42
42
  ### Incorrect
43
43
 
44
- With `shellVersion: "3.2"`:
44
+ With `minBashVersion: "3.2"`:
45
45
 
46
- <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 2} -->
46
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2", "findings": 2} -->
47
47
  ```sh
48
48
  set_result() {
49
49
  local -n out=$1
@@ -54,9 +54,9 @@ declare -n alias=target
54
54
 
55
55
  ### Correct
56
56
 
57
- With `shellVersion: "3.2"`:
57
+ With `minBashVersion: "3.2"`:
58
58
 
59
- <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
59
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2"} -->
60
60
  ```sh
61
61
  set_result() {
62
62
  printf -v "$1" '%s' done
@@ -66,11 +66,11 @@ read_by_name() {
66
66
  }
67
67
  ```
68
68
 
69
- With `shellVersion: "4.3"` or newer, or with no `shellVersion`, namerefs are fine.
69
+ With `minBashVersion: "4.3"` or newer, or with no `minBashVersion`, namerefs are fine.
70
70
 
71
71
  ## When not to use it
72
72
 
73
- Turn the rule off when your scripts never run on Bash older than 4.3, or leave `shellVersion`
73
+ Turn the rule off when your scripts never run on Bash older than 4.3, or leave `minBashVersion`
74
74
  unset, which makes the rule report nothing.
75
75
 
76
76
  ## Relationship to shellcheck
@@ -78,4 +78,4 @@ unset, which makes the rule report nothing.
78
78
  eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
79
79
  layer. Run shellcheck beside it.
80
80
 
81
- Under `-s sh`, shellcheck 0.11.0 reports the command, not the option: `local -n` draws SC3043 (`local` is undefined in POSIX sh), and `declare -n` and `typeset -n` draw SC3044. Under `-s bash` it reports nothing for a nameref. It does not know which Bash release a script must run on, so it reports nothing for a nameref in a Bash 4.2 or 3.2 target. This rule exists for that target.
81
+ Under `-s sh`, shellcheck 0.11.0 reports the command, not the option: `local -n` draws SC3043 (`local` is undefined in POSIX sh), and `declare -n` and `typeset -n` draw SC3044. Under `-s bash` it reports nothing for a nameref. It does not know which Bash release a script must run on, so it reports nothing for a nameref in a script that must support Bash 4.2 or 3.2. This rule checks that support requirement.
@@ -10,15 +10,15 @@ whose target shell lacks them: Bash older than 4.3, including the Bash 3.2 that
10
10
 
11
11
  ## Rule details
12
12
 
13
- Negative subscripts count back from the end of an indexed array. Bash NEWS for 4.3 introduces assigning, referencing and unsetting elements this way; Bash 4.2 already reads `${arr[-1]}`, but rejects the element length `${#arr[-1]}`, assigning and unsetting with `bad array subscript` (see the [evidence table](../../compat.md#evidence)). The rule uses 4.3, the first release where every use works, so a plain `${arr[-1]}` read at `shellVersion: "4.2"` is reported although 4.2 accepts it. Bash 3.2 fails with `bad array subscript`.
13
+ Negative subscripts count back from the end of an indexed array. Bash NEWS for 4.3 introduces assigning, referencing and unsetting elements this way; Bash 4.2 already reads `${arr[-1]}`, but rejects the element length `${#arr[-1]}`, assigning and unsetting with `bad array subscript` (see the [evidence table](../../compat.md#evidence)). The rule uses 4.3, the first release where every use works, so a plain `${arr[-1]}` read at `minBashVersion: "4.2"` is reported although 4.2 accepts it. Bash 3.2 fails with `bad array subscript`.
14
14
 
15
- The rule reports a subscript that is a minus sign followed by a static decimal integer, on an expansion (`${arr[-1]}`, `${#arr[-1]}`) or on an assignment (`arr[-1]=x`). It also reports unsetting an element, `unset arr[-1]` (quoted or not, also after options such as `-v` and after `--`), which Bash NEWS for 4.3 covers; that form has no suggestion. It does not report `-0` (zero, which Bash 3.2 accepts), `${v: -1}` (a substring offset, not a subscript), a computed `-$n`, or a parenthesized `-(1)`. An associative array's `-1` key is a plain string; the rule cannot tell and reports it, but associative arrays need Bash 4.0 anyway.
15
+ The rule reports a subscript that is a minus sign followed by a nonzero static integer (decimal, octal, hexadecimal, or Bash `base#digits` for bases 2–64), on an expansion (`${arr[-1]}`, `${#arr[-1]}`) or on an assignment (`arr[-1]=x`). It also reports unsetting an element, `unset arr[-1]` (quoted or not, also after options such as `-v` and after `--`), which Bash NEWS for 4.3 covers; that form has no suggestion. It does not evaluate arithmetic expressions or machine-integer overflow. Invalid integer spellings such as `08`, `2#2`, or a leading-zero base prefix (`02#1`, `08#1`) are not evidence of a negative index. It does not report zero in any base (`-0`, `-00`, `-0x0`, `-2#0`, which Bash 3.2 accepts), `${v: -1}` (a substring offset, not a subscript), a computed `-$n`, or a parenthesized `-(1)`. An associative array's `-1` key is a plain string; the rule cannot tell and reports it, but associative arrays need Bash 4.0 anyway.
16
16
 
17
17
  ### Suggestion
18
18
 
19
19
  For a bare `${NAME[-N]}` read, the rule suggests counting from the length:
20
20
 
21
- <!-- transform: {"kind": "suggestion", "dialect": "bash", "shellVersion": "3.2"} -->
21
+ <!-- transform: {"kind": "suggestion", "dialect": "bash", "minBashVersion": "3.2"} -->
22
22
 
23
23
  ```sh
24
24
  echo "${arr[-1]}"
@@ -31,19 +31,19 @@ Negative subscripts count back from the highest assigned index plus one, while `
31
31
  This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
32
32
  described in [version compatibility](../../compat.md):
33
33
 
34
- - `shellVersion` unset: never reports. An unset version means the newest Bash.
35
- - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.3`.
34
+ - `minBashVersion` unset: no Bash version constraint, so no report under `bash` or `bats`.
35
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `minBashVersion` is older than `4.3`.
36
36
  - `dialect: "posix"`: reports nothing in practice, because POSIX sh has no arrays, because the POSIX parser rejects it with a parse error before any rule runs.
37
37
  - `dialect: "mksh"` or `"zsh"`: never reports.
38
38
 
39
39
  ## Options
40
40
 
41
- This rule has no options. Set the target through `languageOptions`:
41
+ This rule has no options. Set the required support through `languageOptions`:
42
42
 
43
43
  ```js
44
44
  // eslint.config.js
45
45
  {
46
- languageOptions: { dialect: 'bash', shellVersion: '3.2' },
46
+ languageOptions: { dialect: 'bash', minBashVersion: '3.2' },
47
47
  rules: {
48
48
  'sh/compat/no-negative-array-index': 'error',
49
49
  },
@@ -54,9 +54,9 @@ This rule has no options. Set the target through `languageOptions`:
54
54
 
55
55
  ### Incorrect
56
56
 
57
- With `shellVersion: "3.2"`:
57
+ With `minBashVersion: "3.2"`:
58
58
 
59
- <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 2} -->
59
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2", "findings": 2} -->
60
60
  ```sh
61
61
  last=${items[-1]}
62
62
  items[-1]=done
@@ -64,24 +64,24 @@ items[-1]=done
64
64
 
65
65
  ### Correct
66
66
 
67
- With `shellVersion: "3.2"`:
67
+ With `minBashVersion: "3.2"`:
68
68
 
69
- <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
69
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2"} -->
70
70
  ```sh
71
71
  last=${items[$((${#items[@]}-1))]}
72
72
  items[$((${#items[@]}-1))]=done
73
73
  ```
74
74
 
75
- With `shellVersion: "4.3"` or newer, or with no `shellVersion`, the original forms are fine.
75
+ With `minBashVersion: "4.3"` or newer, or with no `minBashVersion`, the original forms are fine.
76
76
 
77
77
  ## When not to use it
78
78
 
79
79
  Turn the rule off when your scripts never run on Bash older than 4.3, or leave
80
- `shellVersion` unset, which makes the rule report nothing.
80
+ `minBashVersion` unset, which makes the rule report nothing.
81
81
 
82
82
  ## Relationship to shellcheck
83
83
 
84
84
  eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
85
85
  layer. Run shellcheck beside it.
86
86
 
87
- shellcheck reports negative array subscripts in scripts it treats as POSIX sh (SC3054), but it cannot target a Bash version, so it cannot report `${arr[-1]}` for a Bash 3.2 target. This rule exists for that target.
87
+ shellcheck reports negative array subscripts in scripts it treats as POSIX sh (SC3054), but it cannot check support for a minimum Bash version, so it cannot report `${arr[-1]}` for a script that must support Bash 3.2. This rule checks that support requirement.
@@ -6,18 +6,33 @@ ships as `/bin/bash`.
6
6
 
7
7
  | Configs | Fixable | Languages |
8
8
  | ------- | ------- | --------- |
9
- | `bash-3.2`, `posix-portable` | suggestions | `sh/sh` |
9
+ | `posix-portable` | suggestions | `sh/sh` |
10
10
 
11
11
  ## Rule details
12
12
 
13
13
  The syntax is in every Bash, but through Bash 4.3, expanding an empty array while `nounset` is on aborts the script with `a[@]: unbound variable`. Bash 4.4 is the first release where it expands to nothing. This is the classic "works on Linux CI, breaks on a Mac" failure: a script that runs under `set -euo pipefail` and passes an optional, possibly empty, argument array dies on macOS `/bin/bash`.
14
14
 
15
- The rule reports when both of these hold:
15
+ The rule considers an expansion when both of these hold:
16
16
 
17
- - the file turns `nounset` on somewhere: a `set` command that enables it (`set -u`, `set -eu`, `set -o nounset`), or a shebang that passes the flag (`#!/bin/bash -eu`, or `#!/usr/bin/env -S bash -eu`). Where in the file that happens is not considered, because a function defined before `set -u` can run after it;
17
+ - the file turns `nounset` on somewhere: a `set` command that enables it (`set -u`, `set -eu`, `set -o nounset`), or a shebang that passes the flag (`#!/bin/bash -eu`, or `#!/usr/bin/env -S bash -eu`). This is the conservative starting point because a function defined before `set -u` can run after it;
18
18
  - an expansion is `${name[@]}` or `${name[*]}` with no operator, in quotes or not.
19
19
 
20
- A linter cannot know whether an array is empty when the script runs, so every such expansion reports. The check is file-wide: it ignores `set +u`, so an expansion after the file turned `nounset` back off still reports. It also reports arrays that are visibly non-empty (`a=(x); "${a[@]}"`) and arrays already guarded by a length test (`[[ ${#a[@]} -eq 0 ]] && return`). Its message therefore says "If this array is empty", not that it is. These forms work on an empty array under `set -u` in every Bash, and are not reported: `${#a[@]}`, `${!a[@]}`, `${a[@]-default}`, and the guarded forms below, including an expansion inside `${a[@]+…}` or `${a[@]:+…}` of the same array. The slice `${a[@]:1}` and the replacement `${a[@]/x/y}` were observed to work on Bash 3.2 to 4.1 and fail only on 4.2 and 4.3, which a minimum-version gate cannot express, so they are not reported either.
20
+ The rule suppresses a report only when a narrow local proof establishes safety:
21
+
22
+ - a preceding static `set +u` or `set +o nounset` leaves nounset off;
23
+ - an ordinary assignment such as `a=(x)` or `a=("")` guarantees a member. `FUNCNAME` is excluded because it does not obey ordinary assignment semantics. Every element must be a static word; quoted empty strings count, while computed words and unquoted globs do not;
24
+ - `[ "${#a[@]}" -eq 0 ] && return` exits the current function on the empty path, or the same zero-length check uses `exit`. `test` and `[[ ... ]]` spellings are also recognized. With `[` or `test`, the length operand must be an ordinary double-quoted word: unquoted counts can split under a changed `IFS`, making the test fail without proving the array non-empty. An optional literal numeric return/exit status is allowed;
25
+ - the expansion is in the then arm of a single positive length test: `[ "${#a[@]}" -gt 0 ]`, `[[ ${#a[@]} -ne 0 ]]`, or `(( ${#a[@]} ))`, for example.
26
+
27
+ Facts stay in one sequential statement list. A simple assignment invalidates the fact for its target, so `a=(x); a=()` still reports. Static `set` commands apply option changes in order, so re-enabling nounset removes the off proof. Other calls, including function calls, `unset`, and `shopt`, and compound statements discard facts. Assignments and option changes in a function, loop, branch, or subshell do not establish facts in the surrounding list. A loop's word list can use facts from immediately before the loop; its body starts afresh. Redirections, background statements, prefix assignments on the expanding command, arithmetic expansions, indirect expansions, localized strings, or other potentially mutating argument expansions prevent a proof.
28
+
29
+ A top-level `return` is not an exit proof: when the script is executed rather than sourced, Bash rejects it and continues to the expansion. For example, `set -u; a=(); [ "${#a[@]}" -eq 0 ] && return; echo "${a[@]}"` still reports. Put that guard inside a function or use `exit` when the intended behavior is to exit the script.
30
+
31
+ The rule does not interpret function bodies at call sites, follow sourced files, or prove data returned by other commands. Dynamic command names, actual calls to `eval`, `source`, `.`, `trap`, `alias`, `unalias`, `enable`, `fc`, `read`, `mapfile`, `readarray`, `getopts`, `unset`, or `let`, indirect expansions (including key/name forms), localized strings, writes or `for`/`select` bindings to special `BASH_*` variables, the executable `PS4` trace prefix, or writable intrinsic integers (`OPTIND`, `RANDOM`, `SECONDS`), definitions overriding proof builtins, function names outside plain shell identifiers, and unrecognized wrapper flags disable local proofs for the file, including when reached through `builtin` or `command` wrappers. Wrapped declaration commands, and declarations using readonly, integer, associative-array or reference attributes or unmodeled arguments, disable local proofs for the file. A reference or integer variable can write interpreter arrays indirectly, so a later `set +u` is not trusted either. Ordinary `local a` and `local -a a` declarations do not block a subsequent literal assignment proof. Unmodeled arithmetic (including numeric `[[ ... ]]` operands, array indices, slices and `let`), `[[ -v ... ]]`, and coprocesses also disable local proofs because they can evaluate variable text or change special arrays. Direct array lengths and literal nonnegative integers remain supported in arithmetic tests. A `printf` call is considered inert only with a static format made of literal text, `%%`, and the simple string conversions `%s`, `%b`, `%q`, or `%c`; `-v` requires a plain ordinary variable name. Other formats, interpreter-backed or unknown destinations and `%n` can write interpreter state, so they disable local proofs, including through wrappers. Command-based `[` and `test` forms require fixed non-name-lookup operators and operands known to be one word (static or a single double-quoted scalar word). `-v`/`-R`, unknown operators, multiword expansions and unmodeled compound tests disable local proofs: their argument parsing can reach arithmetic variable-name evaluation. These restrictions avoid trusting an assignment that can fail, write through a reference, or run under changed interpreter behavior. Ordinary builtin behavior and ordinary variables are assumed when the file contains no evidence to the contrary; this is not a model of arbitrary startup or sourced-caller state.
32
+
33
+ Consequently, guarded code can still report where the proof would require another execution path, an intervening call, a caller contract, or one of the excluded constructs. The message says the expansion is not proven safe and remains conditional on the array actually being empty and nounset being on.
34
+
35
+ These forms work on an empty array under `set -u` in every Bash, and are not reported: `${#a[@]}`, `${!a[@]}`, `${a[@]-default}`, and the guarded forms below, including an expansion inside `${a[@]+…}` or `${a[@]:+…}` of the same array. The slice `${a[@]:1}` and the replacement `${a[@]/x/y}` were observed to work on Bash 3.2 to 4.1 and fail only on 4.2 and 4.3, which a minimum-version gate cannot express, so they are not reported either.
21
36
 
22
37
  The rule offers a guarded spelling as a suggestion when the word is exactly the quoted expansion:
23
38
 
@@ -29,15 +44,15 @@ An unquoted expansion, or one inside a longer word, reports with no suggestion.
29
44
  This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
30
45
  described in [version compatibility](../../compat.md):
31
46
 
32
- - `shellVersion` unset: never reports. An unset version means the newest Bash.
33
- - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.4`.
47
+ - `minBashVersion` unset: no Bash version constraint, so no report under `bash` or `bats`.
48
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `minBashVersion` is older than `4.4`.
34
49
  - `dialect: "mksh"` or `"zsh"`: never reports.
35
50
 
36
51
  ### Suggestion
37
52
 
38
53
  A suggestion rewrites the quoted `[@]` form:
39
54
 
40
- <!-- transform: {"kind": "suggestion", "dialect": "bash", "shellVersion": "3.2"} -->
55
+ <!-- transform: {"kind": "suggestion", "dialect": "bash", "minBashVersion": "3.2"} -->
41
56
  ```sh
42
57
  set -u
43
58
  run_tool "${extra[@]}"
@@ -48,25 +63,37 @@ run_tool ${extra[@]+"${extra[@]}"}
48
63
 
49
64
  ## Options
50
65
 
51
- This rule has no options. Set the target through `languageOptions`:
66
+ This rule has no options. It is an explicit opt-in for `bash-3.2`. Enable it when a script
67
+ runs with `set -u` on Bash below 4.4 and its arrays can really be empty. Its local proofs
68
+ cannot establish every caller or helper contract, so scripts whose arrays are always non-empty
69
+ can still produce noise. `posix-portable` continues to enable it.
70
+
71
+ Extend the preset and enable the rule explicitly:
52
72
 
53
73
  ```js
54
74
  // eslint.config.js
55
- {
56
- languageOptions: { dialect: 'bash', shellVersion: '3.2' },
75
+ import { defineConfig } from 'eslint/config';
76
+ import sh from '@eslint-sh/eslint-plugin';
77
+
78
+ export default defineConfig({
79
+ files: ['**/*.sh'],
80
+ extends: [sh.configs['bash-3.2']],
57
81
  rules: {
58
82
  'sh/compat/no-nounset-empty-array': 'error',
59
83
  },
60
- }
84
+ });
61
85
  ```
62
86
 
87
+ For another target, set `languageOptions.minBashVersion` explicitly. The rule reports only below
88
+ Bash 4.4; enabling it does not change that boundary.
89
+
63
90
  ## Examples
64
91
 
65
92
  ### Incorrect
66
93
 
67
- With `shellVersion: "3.2"`:
94
+ With `minBashVersion: "3.2"`:
68
95
 
69
- <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 2} -->
96
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2", "findings": 2} -->
70
97
  ```sh
71
98
  set -euo pipefail
72
99
  extra=()
@@ -76,9 +103,9 @@ echo "args: ${extra[*]}"
76
103
 
77
104
  ### Correct
78
105
 
79
- With `shellVersion: "3.2"`:
106
+ With `minBashVersion: "3.2"`:
80
107
 
81
- <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
108
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2"} -->
82
109
  ```sh
83
110
  set -euo pipefail
84
111
  extra=()
@@ -87,12 +114,40 @@ echo "args: ${extra[*]-}"
87
114
  echo "count: ${#extra[@]}"
88
115
  ```
89
116
 
90
- With `shellVersion: "4.4"` or newer, with no `shellVersion`, or in a file that never turns on
117
+ Local proofs also allow these forms with `minBashVersion: "3.2"`:
118
+
119
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2"} -->
120
+ ```sh
121
+ set -u
122
+ extra=(--verbose)
123
+ run_tool "${extra[@]}"
124
+ ```
125
+
126
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2"} -->
127
+ ```sh
128
+ set -u
129
+ set +u
130
+ extra=()
131
+ run_tool "${extra[@]}"
132
+ ```
133
+
134
+ <!-- example: {"dialect": "bash", "minBashVersion": "3.2"} -->
135
+ ```sh
136
+ run_optional() {
137
+ set -u
138
+ extra=()
139
+ [ "${#extra[@]}" -eq 0 ] && return
140
+ run_tool "${extra[@]}"
141
+ }
142
+ run_optional
143
+ ```
144
+
145
+ With `minBashVersion: "4.4"` or newer, with no `minBashVersion`, or in a file that never turns on
91
146
  `set -u`, whole-array expansions are fine.
92
147
 
93
148
  ## When not to use it
94
149
 
95
- Turn the rule off when your scripts never run on Bash older than 4.4, or leave `shellVersion`
150
+ Turn the rule off when your scripts never run on Bash older than 4.4, or leave `minBashVersion`
96
151
  unset, which makes the rule report nothing.
97
152
 
98
153
  ## Relationship to shellcheck