@eslint-sh/eslint-plugin 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +223 -17
  2. package/dist/command-harvest/builtins/bash.d.ts +36 -0
  3. package/dist/command-harvest/builtins/bash.js +98 -0
  4. package/dist/command-harvest/builtins/bats.d.ts +45 -0
  5. package/dist/command-harvest/builtins/bats.js +33 -0
  6. package/dist/command-harvest/builtins/index.d.ts +18 -0
  7. package/dist/command-harvest/builtins/index.js +39 -0
  8. package/dist/command-harvest/builtins/posix.d.ts +51 -0
  9. package/dist/command-harvest/builtins/posix.js +93 -0
  10. package/dist/command-harvest/harvest-commands.d.ts +66 -0
  11. package/dist/command-harvest/harvest-commands.js +97 -0
  12. package/dist/command-harvest/wrapper-table.d.ts +24 -0
  13. package/dist/command-harvest/wrapper-table.js +49 -0
  14. package/dist/configs/agent-authored.d.ts +41 -0
  15. package/dist/configs/agent-authored.js +33 -0
  16. package/dist/configs/bash-3-2.d.ts +28 -0
  17. package/dist/configs/bash-3-2.js +50 -0
  18. package/dist/configs/posix-portable.d.ts +40 -0
  19. package/dist/configs/posix-portable.js +52 -0
  20. package/dist/configs/recommended.d.ts +52 -0
  21. package/dist/configs/recommended.js +34 -0
  22. package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
  23. package/dist/declaration-conventions/comment-pragma.js +153 -0
  24. package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
  25. package/dist/declaration-conventions/declaration-convention.js +1 -0
  26. package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
  27. package/dist/declaration-conventions/read-declared-commands.js +128 -0
  28. package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
  29. package/dist/eslint-plugin-syntax.d.ts +27 -0
  30. package/dist/eslint-plugin.d.ts +1211 -52
  31. package/dist/index.d.ts +56 -8
  32. package/dist/index.js +53 -6
  33. package/dist/language/command-resolution.d.ts +124 -0
  34. package/dist/language/command-resolution.js +120 -0
  35. package/dist/language/end-of-input-marker.d.ts +29 -0
  36. package/dist/language/end-of-input-marker.js +88 -0
  37. package/dist/language/line-breaks.d.ts +18 -0
  38. package/dist/language/line-breaks.js +22 -0
  39. package/dist/language/lint-view-types.d.ts +104 -0
  40. package/dist/language/lint-view-types.js +1 -0
  41. package/dist/language/non-shell-file-guard.d.ts +35 -0
  42. package/dist/language/non-shell-file-guard.js +59 -0
  43. package/dist/language/selector-classes.d.ts +0 -1
  44. package/dist/language/selector-classes.js +0 -1
  45. package/dist/language/shell-language-options.d.ts +7 -1
  46. package/dist/language/shell-language-options.js +4 -4
  47. package/dist/language/shell-language.d.ts +10 -2
  48. package/dist/language/shell-language.js +59 -8
  49. package/dist/language/shell-source-code.d.ts +342 -5
  50. package/dist/language/shell-source-code.js +559 -30
  51. package/dist/language/source-coordinates.d.ts +13 -2
  52. package/dist/language/source-coordinates.js +68 -14
  53. package/dist/language/syntax-view.d.ts +60 -39
  54. package/dist/language/syntax-view.js +200 -45
  55. package/dist/language/word-resolution.d.ts +4 -3
  56. package/dist/language/word-resolution.js +4 -3
  57. package/dist/rules/compat/create-compat-rule.d.ts +59 -0
  58. package/dist/rules/compat/create-compat-rule.js +65 -0
  59. package/dist/rules/compat/declaration-options.d.ts +19 -0
  60. package/dist/rules/compat/declaration-options.js +110 -0
  61. package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
  62. package/dist/rules/compat/no-append-all-redirect.js +73 -0
  63. package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
  64. package/dist/rules/compat/no-associative-arrays.js +39 -0
  65. package/dist/rules/compat/no-case-attributes.d.ts +12 -0
  66. package/dist/rules/compat/no-case-attributes.js +63 -0
  67. package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
  68. package/dist/rules/compat/no-case-fallthrough.js +67 -0
  69. package/dist/rules/compat/no-case-modification.d.ts +35 -0
  70. package/dist/rules/compat/no-case-modification.js +86 -0
  71. package/dist/rules/compat/no-coproc.d.ts +12 -0
  72. package/dist/rules/compat/no-coproc.js +47 -0
  73. package/dist/rules/compat/no-declare-g.d.ts +12 -0
  74. package/dist/rules/compat/no-declare-g.js +59 -0
  75. package/dist/rules/compat/no-double-bracket.d.ts +12 -0
  76. package/dist/rules/compat/no-double-bracket.js +52 -0
  77. package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
  78. package/dist/rules/compat/no-epoch-variables.js +57 -0
  79. package/dist/rules/compat/no-fd-variable.d.ts +12 -0
  80. package/dist/rules/compat/no-fd-variable.js +48 -0
  81. package/dist/rules/compat/no-globstar.d.ts +12 -0
  82. package/dist/rules/compat/no-globstar.js +39 -0
  83. package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
  84. package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
  85. package/dist/rules/compat/no-let.d.ts +12 -0
  86. package/dist/rules/compat/no-let.js +62 -0
  87. package/dist/rules/compat/no-mapfile.d.ts +13 -0
  88. package/dist/rules/compat/no-mapfile.js +122 -0
  89. package/dist/rules/compat/no-nameref.d.ts +12 -0
  90. package/dist/rules/compat/no-nameref.js +44 -0
  91. package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
  92. package/dist/rules/compat/no-negative-array-index.js +123 -0
  93. package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
  94. package/dist/rules/compat/no-nounset-empty-array.js +158 -0
  95. package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
  96. package/dist/rules/compat/no-parameter-transformation.js +123 -0
  97. package/dist/rules/compat/no-pipe-all.d.ts +13 -0
  98. package/dist/rules/compat/no-pipe-all.js +68 -0
  99. package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
  100. package/dist/rules/compat/no-printf-time-format.js +99 -0
  101. package/dist/rules/compat/no-source.d.ts +13 -0
  102. package/dist/rules/compat/no-source.js +103 -0
  103. package/dist/rules/compat/no-test-v.d.ts +12 -0
  104. package/dist/rules/compat/no-test-v.js +280 -0
  105. package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
  106. package/dist/rules/compat/no-unicode-escape.js +120 -0
  107. package/dist/rules/compat/no-wait-n.d.ts +12 -0
  108. package/dist/rules/compat/no-wait-n.js +73 -0
  109. package/dist/rules/compat/operator-range.d.ts +10 -0
  110. package/dist/rules/compat/operator-range.js +15 -0
  111. package/dist/rules/compat/posix-absent-command.d.ts +12 -0
  112. package/dist/rules/compat/posix-absent-command.js +134 -0
  113. package/dist/rules/compat/shopt-options.d.ts +9 -0
  114. package/dist/rules/compat/shopt-options.js +63 -0
  115. package/dist/rules/compat/version-gates.d.ts +76 -0
  116. package/dist/rules/compat/version-gates.js +197 -0
  117. package/dist/rules/create-rule.d.ts +91 -0
  118. package/dist/rules/create-rule.js +51 -0
  119. package/dist/rules/create-wrapper-rule.d.ts +166 -0
  120. package/dist/rules/create-wrapper-rule.js +407 -0
  121. package/dist/rules/define-shell-rule.d.ts +184 -0
  122. package/dist/rules/define-shell-rule.js +56 -0
  123. package/dist/rules/index.d.ts +26 -0
  124. package/dist/rules/index.js +88 -0
  125. package/dist/rules/max-script-complexity.d.ts +28 -0
  126. package/dist/rules/max-script-complexity.js +130 -0
  127. package/dist/rules/no-dynamic-invocation.d.ts +35 -0
  128. package/dist/rules/no-dynamic-invocation.js +117 -0
  129. package/dist/rules/no-eval.d.ts +13 -0
  130. package/dist/rules/no-eval.js +89 -0
  131. package/dist/rules/no-masked-exit-status.d.ts +21 -0
  132. package/dist/rules/no-masked-exit-status.js +667 -0
  133. package/dist/rules/no-secret-env-reads.d.ts +24 -0
  134. package/dist/rules/no-secret-env-reads.js +133 -0
  135. package/dist/rules/no-secret-flags.d.ts +32 -0
  136. package/dist/rules/no-secret-flags.js +162 -0
  137. package/dist/rules/no-unchecked-cd.d.ts +44 -0
  138. package/dist/rules/no-unchecked-cd.js +250 -0
  139. package/dist/rules/no-undeclared-commands.d.ts +20 -0
  140. package/dist/rules/no-undeclared-commands.js +115 -0
  141. package/dist/rules/no-unused-declarations.d.ts +23 -0
  142. package/dist/rules/no-unused-declarations.js +259 -0
  143. package/dist/rules/prefer-local.d.ts +16 -0
  144. package/dist/rules/prefer-local.js +249 -0
  145. package/dist/rules/quote-expansions.d.ts +13 -0
  146. package/dist/rules/quote-expansions.js +229 -0
  147. package/dist/rules/require-network-wrapper.d.ts +40 -0
  148. package/dist/rules/require-network-wrapper.js +244 -0
  149. package/dist/rules/require-shebang.d.ts +29 -0
  150. package/dist/rules/require-shebang.js +101 -0
  151. package/dist/rules/require-strict-mode.d.ts +52 -0
  152. package/dist/rules/require-strict-mode.js +177 -0
  153. package/dist/rules/require-tty-guard.d.ts +13 -0
  154. package/dist/rules/require-tty-guard.js +289 -0
  155. package/dist/rules/resolve-declaration-convention.d.ts +96 -0
  156. package/dist/rules/resolve-declaration-convention.js +109 -0
  157. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  158. package/dist/rules/resolve-network-wrapper.js +116 -0
  159. package/dist/rules/set-options.d.ts +109 -0
  160. package/dist/rules/set-options.js +201 -0
  161. package/dist/rules/shebang-interpreter.d.ts +18 -0
  162. package/dist/rules/shebang-interpreter.js +81 -0
  163. package/dist/rules/shebang-matches-dialect.d.ts +12 -0
  164. package/dist/rules/shebang-matches-dialect.js +69 -0
  165. package/dist/rules/transparent-wrappers.d.ts +52 -0
  166. package/dist/rules/transparent-wrappers.js +99 -0
  167. package/dist/rules/user-pattern.d.ts +63 -0
  168. package/dist/rules/user-pattern.js +184 -0
  169. package/dist/syntax/analyze.d.ts +19 -0
  170. package/dist/syntax/analyze.js +21 -0
  171. package/dist/syntax/index.d.ts +23 -0
  172. package/dist/syntax/index.js +25 -0
  173. package/dist/version.d.ts +0 -1
  174. package/dist/version.js +0 -1
  175. package/docs/compat.md +347 -0
  176. package/docs/custom-rule-tutorial.md +780 -34
  177. package/docs/declaration-conventions.md +478 -0
  178. package/docs/examples/audit-wrapper-pack.mjs +15 -0
  179. package/docs/examples/dep-line-convention.mjs +22 -0
  180. package/docs/examples/eslint.config.mjs +3 -2
  181. package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
  182. package/docs/examples/no-backticks-rule-tester.mjs +106 -0
  183. package/docs/examples/no-backticks-rule.mjs +60 -0
  184. package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
  185. package/docs/examples/no-function-keyword-rule.mjs +59 -0
  186. package/docs/examples/no-listed-commands-rule.ts +63 -0
  187. package/docs/examples/no-rm-rf-rule.mjs +26 -15
  188. package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
  189. package/docs/examples/sidecar-convention.mjs +38 -0
  190. package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
  191. package/docs/examples/sorted-declarations-rule.mjs +52 -0
  192. package/docs/reading-nodes.md +159 -0
  193. package/docs/rules/README.md +78 -0
  194. package/docs/rules/_template.md +83 -0
  195. package/docs/rules/compat/no-append-all-redirect.md +87 -0
  196. package/docs/rules/compat/no-associative-arrays.md +95 -0
  197. package/docs/rules/compat/no-case-attributes.md +82 -0
  198. package/docs/rules/compat/no-case-fallthrough.md +90 -0
  199. package/docs/rules/compat/no-case-modification.md +74 -0
  200. package/docs/rules/compat/no-coproc.md +76 -0
  201. package/docs/rules/compat/no-declare-g.md +80 -0
  202. package/docs/rules/compat/no-double-bracket.md +72 -0
  203. package/docs/rules/compat/no-epoch-variables.md +79 -0
  204. package/docs/rules/compat/no-fd-variable.md +78 -0
  205. package/docs/rules/compat/no-globstar.md +74 -0
  206. package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
  207. package/docs/rules/compat/no-let.md +78 -0
  208. package/docs/rules/compat/no-mapfile.md +118 -0
  209. package/docs/rules/compat/no-nameref.md +81 -0
  210. package/docs/rules/compat/no-negative-array-index.md +87 -0
  211. package/docs/rules/compat/no-nounset-empty-array.md +103 -0
  212. package/docs/rules/compat/no-parameter-transformation.md +94 -0
  213. package/docs/rules/compat/no-pipe-all.md +85 -0
  214. package/docs/rules/compat/no-printf-time-format.md +75 -0
  215. package/docs/rules/compat/no-source.md +98 -0
  216. package/docs/rules/compat/no-test-v.md +82 -0
  217. package/docs/rules/compat/no-unicode-escape.md +91 -0
  218. package/docs/rules/compat/no-wait-n.md +85 -0
  219. package/docs/rules/max-script-complexity.md +109 -0
  220. package/docs/rules/no-dynamic-invocation.md +139 -0
  221. package/docs/rules/no-eval.md +106 -0
  222. package/docs/rules/no-masked-exit-status.md +239 -0
  223. package/docs/rules/no-secret-env-reads.md +173 -0
  224. package/docs/rules/no-secret-flags.md +169 -0
  225. package/docs/rules/no-unchecked-cd.md +146 -0
  226. package/docs/rules/no-undeclared-commands.md +141 -0
  227. package/docs/rules/no-unused-declarations.md +216 -0
  228. package/docs/rules/prefer-local.md +221 -0
  229. package/docs/rules/quote-expansions.md +157 -0
  230. package/docs/rules/require-network-wrapper.md +171 -0
  231. package/docs/rules/require-shebang.md +157 -0
  232. package/docs/rules/require-strict-mode.md +253 -0
  233. package/docs/rules/require-tty-guard.md +128 -0
  234. package/docs/rules/shebang-matches-dialect.md +120 -0
  235. package/docs/selectors.md +2 -2
  236. package/docs/shellcheck.md +86 -0
  237. package/docs/wrapper-rules.md +206 -0
  238. package/package.json +26 -20
  239. package/dist/index.d.ts.map +0 -1
  240. package/dist/index.js.map +0 -1
  241. package/dist/language/selector-classes.d.ts.map +0 -1
  242. package/dist/language/selector-classes.js.map +0 -1
  243. package/dist/language/shell-language-options.d.ts.map +0 -1
  244. package/dist/language/shell-language-options.js.map +0 -1
  245. package/dist/language/shell-language.d.ts.map +0 -1
  246. package/dist/language/shell-language.js.map +0 -1
  247. package/dist/language/shell-source-code.d.ts.map +0 -1
  248. package/dist/language/shell-source-code.js.map +0 -1
  249. package/dist/language/source-coordinates.d.ts.map +0 -1
  250. package/dist/language/source-coordinates.js.map +0 -1
  251. package/dist/language/syntax-view.d.ts.map +0 -1
  252. package/dist/language/syntax-view.js.map +0 -1
  253. package/dist/language/word-resolution.d.ts.map +0 -1
  254. package/dist/language/word-resolution.js.map +0 -1
  255. package/dist/version.d.ts.map +0 -1
  256. package/dist/version.js.map +0 -1
@@ -0,0 +1,159 @@
1
+ # Reading nodes
2
+
3
+ A rule's listeners receive **lint-view nodes**: the plugin's own projection of the parsed script,
4
+ typed so you can read a node's fields without a cast or a hand-written guard. This page is the
5
+ field reference and one worked rule.
6
+
7
+ ## The shape of a node
8
+
9
+ Every node has:
10
+
11
+ - `type` — the syntax kind (`'Stmt'`, `'CallExpr'`, `'Word'`, `'Lit'`, …). It is the discriminator.
12
+ - `range` — a half-open `[start, end]` span of UTF-16 offsets into the source text.
13
+ - `loc` — the same span as ESLint `{ start: { line, column }, end: { line, column } }`, both
14
+ one-based.
15
+
16
+ A listener keyed by a node type, with or without `:exit`, receives exactly that node type, so its
17
+ fields are typed with nothing to check first:
18
+
19
+ ```ts
20
+ CallExpr(node) {
21
+ node.args; // readonly Word nodes
22
+ node.assigns; // readonly Assign nodes
23
+ }
24
+ ```
25
+
26
+ Any other selector (`*`, `CallExpr[args.0.parts.0.value='jq']`, `:command`, `Stmt > CallExpr`)
27
+ can match nodes of more than one type, so its listener receives `ShellNode`, the union of every
28
+ node type. **Narrow it on `type`** and the node's own fields appear, typed:
29
+
30
+ ```ts
31
+ ':command'(node) {
32
+ if (node.type !== 'CallExpr') return;
33
+ node.args;
34
+ }
35
+ ```
36
+
37
+ `ShellNodeOf<'Stmt'>` names one node type (for a helper's parameter), and `ShellNodeType` is the
38
+ union of every `type` value. Field access that does not exist is a compile error, so a typo or a
39
+ field from another node type is caught by the compiler.
40
+
41
+ ## Where the fields come from
42
+
43
+ The nodes mirror [mvdan/sh](https://github.com/mvdan/sh)'s syntax tree, as `sh-ast` exposes it.
44
+ An upstream field `Cmd` on `Stmt` is `stmt.cmd` on the node: **the same name, lowercased**. The
45
+ rules that turn an upstream record into a lint-view node are fixed:
46
+
47
+ | Upstream field | On the node |
48
+ | ----------------------------- | ---------------------------------------------------------------- |
49
+ | `Type` | `type`, the discriminator |
50
+ | a child (`Stmt.Cmd`) | the child node, or **absent** (`undefined`) when there is none |
51
+ | a slice (`CallExpr.Args`) | a read-only array of nodes; **always present**, empty when none |
52
+ | a string (`Lit.Value`) | a JavaScript `string` (decoded from Go bytes) |
53
+ | a boolean (`Stmt.Background`) | a `boolean` |
54
+ | an operator (`BinaryCmd.Op`) | a number; compare with the constants from `…/syntax` (`AndStmt`) |
55
+ | a position (`Stmt.Semicolon`) | none: use `range` and `loc` |
56
+
57
+ A child that may be absent is an **optional** property, so the compiler makes you check it before
58
+ use (`stmt.cmd?.type`). A slice is never absent and never holds a missing entry, so
59
+ `call.args.length` and `for (const word of call.args)` need no check.
60
+
61
+ To find a field, hover the narrowed node in your editor, or read the `sh-ast` record of the same
62
+ name: its field list is the node's field list, minus positions, lowercased.
63
+
64
+ ## What the view adapts
65
+
66
+ The types describe the view as it is, not the raw parse:
67
+
68
+ - `range` and `loc` are in UTF-16 units of the file's text, not the parser's byte offsets.
69
+ - A CRLF file reads like its LF twin. The parser treats only `\n` as a line break, so it keeps the
70
+ `\r` before it as part of whatever ends the line. The view ends such a node **before** the `\r`,
71
+ and a text field of that node (`Lit.value`) loses that one trailing `\r`. A quoted string or
72
+ here-document that continues past the line break is left alone.
73
+ - Some nodes have no source extent of their own (`Expansion`, `Replace`, `Slice` are parts of a
74
+ parameter expansion). Their range covers their children.
75
+
76
+ For anything the view leaves out (token positions, the exact bytes of a non-UTF-8 string), reach
77
+ the parser's own record with `context.sourceCode.getSyntax(node)`; see the [custom-rule
78
+ tutorial](./custom-rule-tutorial.md).
79
+
80
+ ## `range` goes straight into `fixer`
81
+
82
+ `range` has the type ESLint's fixer methods take, so `node.range` is accepted by
83
+ `insertTextBeforeRange`, `insertTextAfterRange`, `removeRange` and `replaceTextRange` as is.
84
+ Treat it as read-only.
85
+
86
+ ## Example: quote the variables `rm` receives
87
+
88
+ `docs/examples/no-unquoted-rm-variable-rule.ts`:
89
+
90
+ ```ts
91
+ /**
92
+ * The TypeScript example rule from "Reading nodes" (`../reading-nodes.md`).
93
+ * This package's test suite asserts the page's code fence matches this
94
+ * file byte-for-byte, type-checks it against the package's published
95
+ * declarations, and runs it, with its fix, through a real `eslint` `Linter`.
96
+ *
97
+ * It reports an `rm` argument that is a lone, unquoted variable (`rm -rf $dir`)
98
+ * and quotes it. Every field it reads is typed by `ShellNode`: narrowing on
99
+ * `type` is the only thing between a listener's node and its fields.
100
+ *
101
+ * @see ../reading-nodes.md — the page this rule backs
102
+ */
103
+ import { defineShellRule } from '@eslint-sh/eslint-plugin';
104
+
105
+ const noUnquotedRmVariable = defineShellRule({
106
+ meta: {
107
+ type: 'problem',
108
+ fixable: 'code',
109
+ docs: { description: 'require the variable operands of `rm` to be quoted' },
110
+ schema: [],
111
+ messages: {
112
+ unquoted: 'Quote `${{name}}`: unquoted, it is split and globbed before `rm` runs.',
113
+ },
114
+ },
115
+ create(context) {
116
+ return {
117
+ CallExpr(node) {
118
+ // A listener keyed by a node type receives that node type: `args`, `assigns`, … are typed.
119
+ if (context.sourceCode.getCommandName(node) !== 'rm') return;
120
+
121
+ // `args` is always an array of `Word`s (the command word first), never null.
122
+ for (const word of node.args.slice(1)) {
123
+ // A `Word` is a list of parts; a lone `$name` is a single `ParamExp` part.
124
+ const [part] = word.parts;
125
+ if (word.parts.length !== 1 || part.type !== 'ParamExp') continue;
126
+
127
+ context.report({
128
+ node: word,
129
+ messageId: 'unquoted',
130
+ // `param` is optional: a `ParamExp` with no name (rare) has no `param` child.
131
+ data: { name: part.param?.value ?? '' },
132
+ // `word.range` is the tuple ESLint's fixer methods take, so it needs no cast.
133
+ fix: (fixer) => [
134
+ fixer.insertTextBeforeRange(word.range, '"'),
135
+ fixer.insertTextAfterRange(word.range, '"'),
136
+ ],
137
+ });
138
+ }
139
+ },
140
+ };
141
+ },
142
+ });
143
+
144
+ export default noUnquotedRmVariable;
145
+ ```
146
+
147
+ Enable it like any rule in step 3 of the [tutorial](./custom-rule-tutorial.md#3-wire-it-into-a-flat-config).
148
+ For `rm -rf $dir "$kept" ${base}/x` it reports only `$dir`: `"$kept"` is a `DblQuoted` part, and
149
+ `${base}/x` has two parts. Its fix produces `rm -rf "$dir" "$kept" ${base}/x`.
150
+
151
+ What the rule reads, in order: the `CallExpr` key gives the listener a `CallExpr` node, so
152
+ `node.args` is its `Word`s; `word.parts` is a `Word`'s pieces, which `part.type` narrows again
153
+ (`'ParamExp'`); `part.param` is optional, so it is read with `?.`.
154
+
155
+ ## Staying current with mvdan/sh
156
+
157
+ The node types are computed from `sh-ast`'s generated typings; nothing in this package restates a
158
+ field list. When the pinned `sh-ast` moves to a new mvdan/sh, the types follow it, and a rule that
159
+ reads a field the new version removed stops compiling instead of reading `undefined` at run time.
@@ -0,0 +1,78 @@
1
+ # Built-in rules
2
+
3
+ eslint-sh enforces shell script conventions and contracts. Run shellcheck alongside it for
4
+ general shell correctness checks. Start with `sh.configs.recommended`; use
5
+ `sh.configs['agent-authored']` for machine-written scripts that people review, and
6
+ `sh.configs['bash-3.2']` or `sh.configs['posix-portable']` for a Bash 3.2 or POSIX `sh`
7
+ deployment target. Every preset includes the shell language and plugin registration; extend them
8
+ under your own `files` globs. See [running eslint-sh beside shellcheck](../shellcheck.md).
9
+ See the [canonical quickstart](https://github.com/mike-north/eslint-sh/blob/main/packages/eslint-sh/README.md).
10
+
11
+ | Rule | Configs | Autofix | Suggestions | Purpose |
12
+ | ---- | ------- | ------- | ----------- | ------- |
13
+ | [`sh/compat/no-append-all-redirect`](./compat/no-append-all-redirect.md) | `bash-3.2`, `posix-portable` | No | Yes | Disallow the `&>>` redirection when the target shell lacks it, suggesting `>> file 2>&1`. |
14
+ | [`sh/compat/no-associative-arrays`](./compat/no-associative-arrays.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `declare -A` / `typeset -A` when the target shell lacks associative arrays. |
15
+ | [`sh/compat/no-case-attributes`](./compat/no-case-attributes.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `declare -l` / `declare -u` (and `typeset`, `local`) when the target shell lacks them, pointing at `tr`. |
16
+ | [`sh/compat/no-case-fallthrough`](./compat/no-case-fallthrough.md) | `bash-3.2`, `posix-portable` | No | No | Disallow the `case` terminators `;&` and `;;&` when the target shell lacks them. |
17
+ | [`sh/compat/no-case-modification`](./compat/no-case-modification.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `${v^^}`, `${v,,}` and variants when the target shell lacks them, pointing at `tr`. |
18
+ | [`sh/compat/no-coproc`](./compat/no-coproc.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `coproc` when the target shell lacks it. |
19
+ | [`sh/compat/no-declare-g`](./compat/no-declare-g.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `declare -g` / `typeset -g` when the target shell lacks the option. |
20
+ | [`sh/compat/no-double-bracket`](./compat/no-double-bracket.md) | `posix-portable` | No | No | Disallow `[[ … ]]` when the target shell is POSIX sh, pointing at `[` and `case`. |
21
+ | [`sh/compat/no-epoch-variables`](./compat/no-epoch-variables.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `$EPOCHSECONDS` / `$EPOCHREALTIME` when the target shell lacks them. |
22
+ | [`sh/compat/no-fd-variable`](./compat/no-fd-variable.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `{varname}` descriptor redirections (`exec {fd}>file`) when the target shell lacks them. |
23
+ | [`sh/compat/no-globstar`](./compat/no-globstar.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `shopt -s globstar` when the target shell lacks the option, pointing at `find`. |
24
+ | [`sh/compat/no-lastpipe-semantics`](./compat/no-lastpipe-semantics.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `shopt -s lastpipe` when the target shell lacks the option. |
25
+ | [`sh/compat/no-let`](./compat/no-let.md) | `posix-portable` | No | No | Disallow `let` when the target shell is POSIX sh, pointing at arithmetic expansion. |
26
+ | [`sh/compat/no-mapfile`](./compat/no-mapfile.md) | `bash-3.2`, `posix-portable` | No | Yes | Disallow `mapfile` / `readarray` when the target shell lacks them, suggesting a read loop. |
27
+ | [`sh/compat/no-nameref`](./compat/no-nameref.md) | `bash-3.2`, `posix-portable` | No | No | Disallow namerefs (`declare -n`, `local -n`) when the target shell lacks them. |
28
+ | [`sh/compat/no-negative-array-index`](./compat/no-negative-array-index.md) | `bash-3.2`, `posix-portable` | No | Yes | Disallow negative array subscripts when the target shell lacks them, suggesting a length-based index. |
29
+ | [`sh/compat/no-nounset-empty-array`](./compat/no-nounset-empty-array.md) | `bash-3.2`, `posix-portable` | No | Yes | Disallow whole-array expansions under `set -u` when the target shell aborts on an empty array, suggesting a guarded form. |
30
+ | [`sh/compat/no-parameter-transformation`](./compat/no-parameter-transformation.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `${v@op}` transformations when the target shell lacks the operator. |
31
+ | [`sh/compat/no-pipe-all`](./compat/no-pipe-all.md) | `bash-3.2`, `posix-portable` | No | Yes | Disallow the stderr-merging pipe (pipe-ampersand) when the target shell lacks it, suggesting an explicit `2>&1` before the pipe. |
32
+ | [`sh/compat/no-printf-time-format`](./compat/no-printf-time-format.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `printf`'s `%(fmt)T` date format when the target shell lacks it. |
33
+ | [`sh/compat/no-source`](./compat/no-source.md) | `posix-portable` | No | Yes | Disallow `source` when the target shell is POSIX sh, suggesting `.`. |
34
+ | [`sh/compat/no-test-v`](./compat/no-test-v.md) | `bash-3.2`, `posix-portable` | No | No | Disallow the `-v name` test (`[[ -v x ]]`, `test -v x`) when the target shell lacks it. |
35
+ | [`sh/compat/no-unicode-escape`](./compat/no-unicode-escape.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `\u` / `\U` escapes in `$'…'` when the target shell does not decode them to UTF-8. |
36
+ | [`sh/compat/no-wait-n`](./compat/no-wait-n.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `wait -n` when the target shell lacks it. |
37
+ | [`sh/max-script-complexity`](./max-script-complexity.md) | `agent-authored` | No | No | Cap a script's function count, nesting depth and statement count. |
38
+ | [`sh/no-dynamic-invocation`](./no-dynamic-invocation.md) | `agent-authored` | No | No | Require each executed command to be statically identifiable, including through wrappers. |
39
+ | [`sh/no-eval`](./no-eval.md) | `recommended`, `agent-authored` | No | No | Disallow `eval` and sourcing a path that is not a static literal. |
40
+ | [`sh/no-masked-exit-status`](./no-masked-exit-status.md) | `agent-authored` | No | Yes | Disallow `local`, `declare`, `typeset`, `export` and `readonly` with a value taken from a command substitution, which hides the command's exit status. |
41
+ | [`sh/no-secret-env-reads`](./no-secret-env-reads.md) | `agent-authored` | No | No | Flag reads of secret-shaped environment variables outside allowed sinks. |
42
+ | [`sh/no-secret-flags`](./no-secret-flags.md) | `recommended`, `agent-authored` | No | No | Flag literal secrets passed through command-line flags. |
43
+ | [`sh/no-unchecked-cd`](./no-unchecked-cd.md) | `recommended`, `agent-authored` | No | Yes | Require the failure of `cd`, `pushd` and `popd` to be checked. |
44
+ | [`sh/no-undeclared-commands`](./no-undeclared-commands.md) | `agent-authored` | No | No | Require external command usage to be declared through your convention. |
45
+ | [`sh/no-unused-declarations`](./no-unused-declarations.md) | `agent-authored` | Yes (when the convention provides an edit hook) | No | Remove command declarations that a fully understood script does not use. |
46
+ | [`sh/prefer-local`](./prefer-local.md) | `agent-authored` | Yes (declaration inserted at the top of the function body) | No | Require variables assigned in a function to be declared `local`. |
47
+ | [`sh/quote-expansions`](./quote-expansions.md) | `recommended`, `agent-authored` | No | Yes | Require double quotes around expansions in words the shell splits and globs. |
48
+ | [`sh/require-network-wrapper`](./require-network-wrapper.md) | `agent-authored` | No | No | Require network commands to run through your configured wrapper. |
49
+ | [`sh/require-shebang`](./require-shebang.md) | `recommended`, `agent-authored` | Yes (missing shebang only) | No | Require a recognized shell interpreter on line 1. |
50
+ | [`sh/require-strict-mode`](./require-strict-mode.md) | `recommended`, `agent-authored`, `posix-portable` (with set -eu) | Yes (when insertion satisfies the rule) | No | Require strict-mode options near the start of the script. |
51
+ | [`sh/require-tty-guard`](./require-tty-guard.md) | `agent-authored` | No | Yes | Require `read`, `select` and configured interactive commands to be guarded by a TTY test or supplied input. |
52
+ | [`sh/shebang-matches-dialect`](./shebang-matches-dialect.md) | `recommended`, `agent-authored`, `posix-portable` | No | No | Require the shebang to name the same kind of shell (bash or POSIX) as `languageOptions.dialect`. |
53
+
54
+ `recommended` enables `no-secret-flags` and `quote-expansions` at `warn` and its other rules at
55
+ `error`. `agent-authored` enables every rule above except the gated `compat/*` rules, all at `error`. `compat/no-append-all-redirect`, `compat/no-mapfile`,
56
+ `compat/no-negative-array-index`, `compat/no-nounset-empty-array`, `compat/no-pipe-all`, `compat/no-source`, `no-masked-exit-status`, `no-unchecked-cd`, `quote-expansions` and `require-tty-guard`
57
+ offer editor suggestions, which you apply one at a time from your editor; the Autofix column
58
+ describes edits available through `eslint --fix`. `quote-expansions` suggests quoting instead of
59
+ fixing because only the script's author knows whether the splitting was intended.
60
+
61
+ The version-gated `compat/*` rules (`compat/no-append-all-redirect`, `compat/no-associative-arrays`, `compat/no-case-attributes`, `compat/no-case-fallthrough`, `compat/no-case-modification`, `compat/no-coproc`, `compat/no-declare-g`, `compat/no-epoch-variables`, `compat/no-fd-variable`, `compat/no-globstar`, `compat/no-lastpipe-semantics`, `compat/no-mapfile`, `compat/no-nameref`, `compat/no-negative-array-index`, `compat/no-nounset-empty-array`, `compat/no-parameter-transformation`, `compat/no-pipe-all`, `compat/no-printf-time-format`, `compat/no-test-v`, `compat/no-unicode-escape`, `compat/no-wait-n`) report
62
+ shell features that your target shell lacks, as set by `languageOptions.shellVersion` and
63
+ `dialect`. They report nothing when `shellVersion` is unset, so the `bash-3.2` preset sets it to `3.2`
64
+ and the `posix-portable` preset sets `dialect: "posix"`; each enables all of them.
65
+ See [version compatibility](../compat.md). `compat/*` means target gates only; `shebang-matches-dialect`
66
+ compares the file with your config whatever the target, so it lives outside that namespace. `posix-portable` also enables
67
+ `shebang-matches-dialect`, `require-strict-mode` (as `set -eu`) and the POSIX-only gates
68
+ `compat/no-double-bracket`, `compat/no-let` and `compat/no-source`, which report those constructs
69
+ under `dialect: "posix"` because they fail on dash. It does not report `local`, `echo -e` or the
70
+ other constructs listed under
71
+ [what `posix-portable` does not report](../compat.md#what-posix-portable-does-not-report).
72
+ To run these rules beside shellcheck, see [shellcheck](../shellcheck.md).
73
+
74
+ `no-undeclared-commands` and `no-unused-declarations` report nothing until you bind
75
+ `settings.sh.declarationConvention` (see [declaration conventions](../declaration-conventions.md)).
76
+ `require-network-wrapper` reports nothing until you set its `wrapper` option (see
77
+ [wrapper rules](../wrapper-rules.md)). Each linked rule page documents its options, limitations,
78
+ and relationship to shellcheck.
@@ -0,0 +1,83 @@
1
+ # `sh/<rule-name>`
2
+
3
+ <!--
4
+ Template for a built-in rule's docs page. Copy it to `docs/rules/<rule-name>.md`
5
+ (the same name the rule is registered under in `src/rules/index.ts`), replace
6
+ every `<...>` placeholder, and delete these comments. The rule's `meta.docs.url`
7
+ already points at that path. A test fails if a registered rule has no page here,
8
+ or a page here has no registered rule; this template is the only exception.
9
+
10
+ Examples are executed. Every `sh` fence under `### Incorrect` must make this rule report
11
+ at least one finding, and every one under `### Correct` must report none, when linted with
12
+ only this rule enabled through the built plugin. A fence that needs options, a dialect or
13
+ a declaration convention says so in an HTML comment on the line above it. The comment is
14
+ the opening "angle bracket, bang, two hyphens", the word `example:`, a JSON object such as
15
+ `{"options": {"maxNesting": 2}, "dialect": "bash"}`, then the usual closing. See any
16
+ existing page, e.g. `max-script-complexity.md`, for the exact spelling.
17
+
18
+ Keys: `options` (the rule's options value), `dialect`, `shellVersion`, `convention`
19
+ (`"commentPragma"`) and `filename`. Under `### Incorrect` you may also state what the prose
20
+ claims exactly: `findings` (a count), `messageIds` (in report order), `output` (the source
21
+ after autofix) and `suggestionOutputs`. A typo in the annotation fails the test.
22
+
23
+ A before/after pair shown elsewhere on the page (a suggestion or autofix result) goes in a
24
+ fence with a `# becomes` line between the two, under a comment spelled like the one above
25
+ but with `transform:` and a `"kind"` of `"suggestion"` or `"fix"` (see `quote-expansions.md`).
26
+
27
+ The first table's `Configs` and `Fixable` cells are checked against the presets and the
28
+ rule's `meta`: start `Fixable` with `not fixable`, `suggestions` or `autofix`.
29
+ -->
30
+
31
+ <One sentence: what the rule reports, phrased as the policy it enforces.>
32
+
33
+ | Configs | Fixable | Languages |
34
+ | ---------------- | -------------------------------------- | --------- |
35
+ | <`recommended`…> | <autofix / suggestions / not fixable> | `sh/sh` |
36
+
37
+ ## Rule details
38
+
39
+ <Why this policy matters for shell scripts, and exactly what the rule reports. Say which
40
+ node types or constructs it inspects and which it deliberately ignores. If the rule depends on
41
+ `languageOptions` (`dialect`, `shellVersion`), say how.>
42
+
43
+ ## Options
44
+
45
+ <Each option: name, type, default, and effect. If the rule has no options, say so:>
46
+
47
+ This rule has no options.
48
+
49
+ ```js
50
+ // eslint.config.js
51
+ {
52
+ rules: {
53
+ 'sh/<rule-name>': ['error', { /* <options> */ }],
54
+ },
55
+ }
56
+ ```
57
+
58
+ ## Examples
59
+
60
+ ### Incorrect
61
+
62
+ ```sh
63
+ # <code this rule reports, with a comment on each reported line saying why>
64
+ ```
65
+
66
+ ### Correct
67
+
68
+ ```sh
69
+ # <code this rule accepts, including the edge cases it intentionally allows>
70
+ ```
71
+
72
+ ## When not to use it
73
+
74
+ <The situations where turning this rule off is the right call, for example a policy your
75
+ project does not hold, or another tool already enforcing it.>
76
+
77
+ ## Relationship to shellcheck
78
+
79
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
80
+ layer. Run shellcheck beside it.
81
+
82
+ <Name any shellcheck check (`SCxxxx`) this rule overlaps, and why this rule exists anyway, or
83
+ state that shellcheck has no equivalent and why it cannot express this policy.>
@@ -0,0 +1,87 @@
1
+ # `sh/compat/no-append-all-redirect`
2
+
3
+ Disallow the `&>>` redirection, which appends both stdout and stderr to a file, in scripts whose
4
+ target shell lacks it: Bash older than 4.0, including the Bash 3.2 that macOS ships as
5
+ `/bin/bash`, and POSIX sh.
6
+
7
+ | Configs | Fixable | Languages |
8
+ | ------- | ------- | --------- |
9
+ | `bash-3.2`, `posix-portable` | suggestions | `sh/sh` |
10
+
11
+ ## Rule details
12
+
13
+ `&>>` arrived in Bash 4.0 (Bash NEWS for 4.0). Bash 3.2 rejects it as a syntax error (``syntax error near unexpected token `>'``), so the whole script fails to run, however rarely the line runs.
14
+
15
+ The rule reports a redirection whose operator is `&>>`; the report is on the whole redirection. The two-way `&>` (which Bash 3.2 has), `>>`, `2>>`, `>&` and `2>&1` are other operators and are not reported, and neither is any of them in quotes.
16
+
17
+ The portable spelling is `>> file 2>&1`, which the Bash manual defines `&>>` to mean. The rule offers it as a suggestion: the operator becomes `>>` and ` 2>&1` follows the target word, so the redirection keeps its place among the command's other redirections (`&>> log 2>/dev/null` becomes `>> log 2>&1 2>/dev/null`, which still sends stderr to `/dev/null`).
18
+
19
+ Under `dialect: "posix"` the parser itself rejects `&>>`, so a POSIX file that uses it fails to parse and this rule does not run on it.
20
+
21
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
22
+ described in [version compatibility](../../compat.md):
23
+
24
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
25
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.0`.
26
+ - `dialect: "mksh"` or `"zsh"`: never reports.
27
+
28
+ ### Suggestion
29
+
30
+ A suggestion rewrites the redirection:
31
+
32
+ <!-- transform: {"kind": "suggestion", "dialect": "bash", "shellVersion": "3.2"} -->
33
+ ```sh
34
+ build &>> build.log
35
+ # becomes
36
+ build >> build.log 2>&1
37
+ ```
38
+
39
+ ## Options
40
+
41
+ This rule has no options. Set the target through `languageOptions`:
42
+
43
+ ```js
44
+ // eslint.config.js
45
+ {
46
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
47
+ rules: {
48
+ 'sh/compat/no-append-all-redirect': 'error',
49
+ },
50
+ }
51
+ ```
52
+
53
+ ## Examples
54
+
55
+ ### Incorrect
56
+
57
+ With `shellVersion: "3.2"`:
58
+
59
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 1} -->
60
+ ```sh
61
+ build &>> build.log
62
+ ```
63
+
64
+ ### Correct
65
+
66
+ With `shellVersion: "3.2"`:
67
+
68
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
69
+ ```sh
70
+ build >> build.log 2>&1
71
+ build &> build.log
72
+ echo "use &>> on Bash 4"
73
+ ```
74
+
75
+ With `shellVersion: "4.0"` or newer, or with no `shellVersion`, `&>>` is fine.
76
+
77
+ ## When not to use it
78
+
79
+ Turn the rule off when your scripts never run on Bash older than 4.0, or leave `shellVersion`
80
+ unset, which makes the rule report nothing.
81
+
82
+ ## Relationship to shellcheck
83
+
84
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
85
+ layer. Run shellcheck beside it.
86
+
87
+ shellcheck 0.11.0 reports nothing for `cmd &>> log` under either `-s sh` or `-s bash`. (`cmd &> log` is different: it reports SC3020 under `-s sh`.) So shellcheck gives no finding for `&>>` in a POSIX sh script or in a Bash 3.2 target. This rule exists for both.
@@ -0,0 +1,95 @@
1
+ # `sh/compat/no-associative-arrays`
2
+
3
+ Disallow associative arrays (`declare -A`, `typeset -A`) in scripts whose target shell lacks
4
+ them: Bash older than 4.0, including the Bash 3.2 that macOS ships as `/bin/bash`, and POSIX sh.
5
+
6
+ | Configs | Fixable | Languages |
7
+ | ------- | ----------- | --------- |
8
+ | `bash-3.2`, `posix-portable` | not fixable | `sh/sh` |
9
+
10
+ ## Rule details
11
+
12
+ Associative arrays arrived in Bash 4.0. Bash 3.2 rejects `declare -A` as an invalid option, so
13
+ the script fails where the array is declared. Worse, if the script keeps going, every later
14
+ `${map[key]}` silently indexes an ordinary array by the arithmetic value of `key`, so lookups
15
+ return the wrong values without an error.
16
+
17
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
18
+ described in [version compatibility](../../compat.md):
19
+
20
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
21
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.0`.
22
+ - `dialect: "posix"`: reports at any `shellVersion`, because POSIX sh has no arrays.
23
+ - `dialect: "mksh"` or `"zsh"`: never reports.
24
+
25
+ The rule reads the options of every `declare` and `typeset` command, up to the first operand,
26
+ as the builtin itself does. Option processing stops at a variable name, at `--`, and at a word
27
+ that is not static. An option word reports when it is a `-` cluster containing `A`, such as
28
+ `-A`, `-gA`, or `-Ar`. The report is on that option word. Commands written through `builtin`
29
+ or `command` are recognized too, and so is `declare` under `dialect: "posix"`, where it parses
30
+ as an ordinary command.
31
+
32
+ The rule deliberately ignores:
33
+
34
+ - `local -A`, `readonly -A`, and `export -A`;
35
+ - `declare -a` (indexed arrays exist in every Bash) and `declare +A`;
36
+ - an `-A` after the first operand (`declare m -A`) or after `--`.
37
+
38
+ ## Options
39
+
40
+ This rule has no options. Set the target through `languageOptions`:
41
+
42
+ ```js
43
+ // eslint.config.js
44
+ {
45
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
46
+ rules: {
47
+ 'sh/compat/no-associative-arrays': 'error',
48
+ },
49
+ }
50
+ ```
51
+
52
+ ## Examples
53
+
54
+ ### Incorrect
55
+
56
+ With `shellVersion: "3.2"`:
57
+
58
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 4} -->
59
+ ```sh
60
+ declare -A ports # associative arrays need Bash 4.0
61
+ typeset -A ports # the same declaration through typeset
62
+ declare -gA ports=([web]=80) # `A` inside an option cluster
63
+ builtin declare -A ports # reached through the builtin wrapper
64
+ ```
65
+
66
+ ### Correct
67
+
68
+ With `shellVersion: "3.2"`:
69
+
70
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
71
+ ```sh
72
+ # Keep the mapping as JSON and look keys up with jq.
73
+ port=$(jq -r --arg k "$service" '.[$k]' ports.json)
74
+
75
+ declare -a hosts # indexed arrays exist in Bash 3.2
76
+ ```
77
+
78
+ With `shellVersion: "4.0"` or newer, or with no `shellVersion`:
79
+
80
+ <!-- example: {"dialect": "bash", "shellVersion": "4.0"} -->
81
+ ```sh
82
+ declare -A ports
83
+ ```
84
+
85
+ ## When not to use it
86
+
87
+ Turn the rule off when your scripts never run on Bash older than 4.0, or leave `shellVersion`
88
+ unset, which makes the rule report nothing.
89
+
90
+ ## Relationship to shellcheck
91
+
92
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
93
+ layer. Run shellcheck beside it.
94
+
95
+ shellcheck 0.11.0 reports the command, not the option: under `-s sh`, `declare -A` draws SC3044 (`declare` is undefined in POSIX sh), and `local -A` draws SC3043. Under `-s bash` it reports nothing. It has no way to target a Bash version, so it reports nothing for `declare -A` in a script that must run on Bash 3.2. This rule exists for that target.
@@ -0,0 +1,82 @@
1
+ # `sh/compat/no-case-attributes`
2
+
3
+ Disallow the case-converting attributes `declare -l` and `declare -u` (also through `typeset` and
4
+ `local`) in scripts whose target shell lacks them: Bash older than 4.0, including the Bash 3.2
5
+ that macOS ships as `/bin/bash`, and POSIX sh.
6
+
7
+ | Configs | Fixable | Languages |
8
+ | ---------------------------- | ----------- | --------- |
9
+ | `bash-3.2`, `posix-portable` | not fixable | `sh/sh` |
10
+
11
+ ## Rule details
12
+
13
+ The `-l` and `-u` attributes arrived in Bash 4.0 (Bash NEWS for 4.0), with the case-modifying expansions that [`sh/compat/no-case-modification`](./no-case-modification.md) reports. Bash 3.2 rejects the option with `declare: -l: invalid option`, so the declaration fails and the variable is left unset.
14
+
15
+ The rule reads the options of every `declare`, `typeset` and `local` command, up to the first operand, as the builtin does. An option word reports when it is a `-` cluster containing `l` or `u` (`-l`, `-u`, `-lx`); the report is on that word. A cluster with both (`-lu`) gets its own message: where the attributes exist, the two cancel out and leave the value as written (observed on Bash 4.0 and 5.3), so the fix is to drop both letters. `-u` of another command (`read -u 3`) and an option after the first operand or after `--` are not read.
16
+
17
+ The portable alternative is to convert the value when it is assigned, with `tr`: `v=$(printf '%s' "$v" | tr '[:upper:]' '[:lower:]')` for `-l`, and `tr '[:lower:]' '[:upper:]'` for `-u`. An attribute converts every later assignment too, so convert at each assignment that needs it.
18
+
19
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
20
+ described in [version compatibility](../../compat.md):
21
+
22
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
23
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.0`.
24
+ - `dialect: "posix"`: reports at any `shellVersion`, because POSIX sh has no `declare`.
25
+ - `dialect: "mksh"` or `"zsh"`: never reports.
26
+
27
+ ## Options
28
+
29
+ This rule has no options. Set the target through `languageOptions`:
30
+
31
+ ```js
32
+ // eslint.config.js
33
+ {
34
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
35
+ rules: {
36
+ 'sh/compat/no-case-attributes': 'error',
37
+ },
38
+ }
39
+ ```
40
+
41
+ ## Examples
42
+
43
+ ### Incorrect
44
+
45
+ With `shellVersion: "3.2"`:
46
+
47
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 2} -->
48
+
49
+ ```sh
50
+ declare -l answer=$1
51
+ f() {
52
+ local -u code=$2
53
+ }
54
+ ```
55
+
56
+ ### Correct
57
+
58
+ With `shellVersion: "3.2"`:
59
+
60
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
61
+
62
+ ```sh
63
+ answer=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
64
+ f() {
65
+ local code
66
+ code=$(printf '%s' "$2" | tr '[:lower:]' '[:upper:]')
67
+ }
68
+ ```
69
+
70
+ With `shellVersion: "4.0"` or newer, or with no `shellVersion`, the attributes are fine.
71
+
72
+ ## When not to use it
73
+
74
+ Turn the rule off when your scripts never run on Bash older than 4.0, or leave `shellVersion`
75
+ unset, which makes the rule report nothing.
76
+
77
+ ## Relationship to shellcheck
78
+
79
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
80
+ layer. Run shellcheck beside it.
81
+
82
+ Under `-s sh`, shellcheck 0.11.0 reports the command, not the option: `declare -l` and `declare -u` draw SC3044 (`declare` is undefined in POSIX sh), and `local -l` and `local -u` draw SC3043. Under `-s bash` it reports nothing for these options. It does not know which Bash release a script must run on, so it reports nothing for `declare -l` in a Bash 3.2 target. This rule exists for that target.