@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,86 @@
1
+ /**
2
+ * `sh/compat/no-case-modification`: reports the case-modifying parameter
3
+ * expansions `${v^}`, `${v^^}`, `${v,}` and `${v,,}` (with or without a
4
+ * pattern) in files whose target shell lacks them, that is, Bash older than
5
+ * 4.0 (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
6
+ *
7
+ * Bash 3.2 rejects the expansion with "bad substitution" at the moment the
8
+ * word is expanded. The message suggests the portable alternative: pipe the
9
+ * value through `tr`. The example it shows is for the operator written: all
10
+ * characters for `^^` and `,,`, the first character for `^` and `,`
11
+ * (`${v%"${v#?}"}` is the first character and `${v#?}` the rest).
12
+ *
13
+ * A pattern form (`${v^^a}`) changes only the characters the pattern matches,
14
+ * which a plain case-converting `tr` does not reproduce, so it gets a message
15
+ * that offers no `tr` equivalent.
16
+ *
17
+ * What is read: a `ParamExp` whose operator is one of the four case
18
+ * operators. The report is on the whole expansion.
19
+ */
20
+ import { UpperAll, UpperFirst, LowerAll, LowerFirst } from 'sh-ast';
21
+ import { createCompatRule } from './create-compat-rule.js';
22
+ /** `tr` arguments that convert to upper case. @internal */
23
+ export const TO_UPPER = "tr '[:lower:]' '[:upper:]'";
24
+ /** `tr` arguments that convert to lower case. @internal */
25
+ export const TO_LOWER = "tr '[:upper:]' '[:lower:]'";
26
+ /** The four case operators, keyed by the parser's operator constant. */
27
+ const CASE_OPERATORS = new Map([
28
+ [UpperAll, { operator: '^^', all: true, tr: TO_UPPER }],
29
+ [UpperFirst, { operator: '^', all: false, tr: TO_UPPER }],
30
+ [LowerAll, { operator: ',,', all: true, tr: TO_LOWER }],
31
+ [LowerFirst, { operator: ',', all: false, tr: TO_LOWER }],
32
+ ]);
33
+ /**
34
+ * The `tr` replacement shown in the message for `name` under `spec`.
35
+ *
36
+ * @internal
37
+ */
38
+ export function trExample(name, spec) {
39
+ const whole = `"$${name}"`;
40
+ return spec.all
41
+ ? `$(printf '%s' ${whole} | ${spec.tr})`
42
+ : `$(printf '%s' "\${${name}%"\${${name}#?}"}" | ${spec.tr})"\${${name}#?}"`;
43
+ }
44
+ /**
45
+ * The `sh/compat/no-case-modification` rule module.
46
+ *
47
+ * @internal
48
+ */
49
+ export const noCaseModification = createCompatRule({
50
+ name: 'compat/no-case-modification',
51
+ feature: 'caseModification',
52
+ meta: {
53
+ type: 'problem',
54
+ docs: {
55
+ description: 'Disallow case-modifying expansions (`${v^^}`, `${v,,}` and variants) when the target shell lacks them',
56
+ },
57
+ messages: {
58
+ caseModification: '`${…{{operator}}}` needs {{required}}, but this file targets {{target}}. Convert the case with `tr` instead, for example {{example}}.',
59
+ caseModificationPattern: '`${…{{operator}}pattern}` needs {{required}}, but this file targets {{target}}. It changes only the characters the pattern matches, so a plain case-converting `tr` is not equivalent; spell out the matching characters instead (for example `tr a A`).',
60
+ },
61
+ },
62
+ create(context, gate) {
63
+ return {
64
+ ParamExp(node) {
65
+ const { exp } = node;
66
+ const spec = exp === undefined ? undefined : CASE_OPERATORS.get(exp.op);
67
+ if (exp === undefined || spec === undefined)
68
+ return;
69
+ const name = node.param?.value ?? 'v';
70
+ if (exp.word !== undefined) {
71
+ context.report({
72
+ node,
73
+ messageId: 'caseModificationPattern',
74
+ data: { ...gate, operator: spec.operator },
75
+ });
76
+ return;
77
+ }
78
+ context.report({
79
+ node,
80
+ messageId: 'caseModification',
81
+ data: { ...gate, operator: spec.operator, example: trExample(name, spec) },
82
+ });
83
+ },
84
+ };
85
+ },
86
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoCoprocMessageId = 'coproc';
7
+ /**
8
+ * The `sh/compat/no-coproc` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noCoproc: import("../define-shell-rule.js").ShellRule<[], "coproc">;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `sh/compat/no-coproc`: reports the `coproc` reserved word (a coprocess:
3
+ * a background command with a pipe pair to the shell) in files whose target
4
+ * shell lacks it, that is, Bash older than 4.0 (macOS's stock `/bin/bash` is
5
+ * 3.2) and POSIX sh.
6
+ *
7
+ * Bash 3.2 does not know the word, so `coproc cat` runs an external command
8
+ * named `coproc` and fails with `command not found`. The message suggests the
9
+ * portable alternative: a named pipe made with `mkfifo`, with the producer
10
+ * started in the background.
11
+ *
12
+ * What is read: Bash's `CoprocClause`. Under `dialect: "posix"` the parser
13
+ * has no such clause and reads `coproc cat` as an ordinary command, so a
14
+ * command whose name is `coproc` is reported too.
15
+ */
16
+ import { createCompatRule } from './create-compat-rule.js';
17
+ /**
18
+ * The `sh/compat/no-coproc` rule module.
19
+ *
20
+ * @internal
21
+ */
22
+ export const noCoproc = createCompatRule({
23
+ name: 'compat/no-coproc',
24
+ feature: 'coproc',
25
+ meta: {
26
+ type: 'problem',
27
+ docs: {
28
+ description: 'Disallow the `coproc` reserved word when the target shell lacks it',
29
+ },
30
+ messages: {
31
+ coproc: '`coproc` needs {{required}}, but this file targets {{target}}. Use a named pipe instead: create it with `mkfifo`, start the coprocess in the background reading or writing the pipe, and open the pipe from the script.',
32
+ },
33
+ },
34
+ create(context, gate) {
35
+ const { sourceCode } = context;
36
+ return {
37
+ CoprocClause(node) {
38
+ context.report({ node, messageId: 'coproc', data: { ...gate } });
39
+ },
40
+ CallExpr(node) {
41
+ if (sourceCode.getCommandName(node) === 'coproc') {
42
+ context.report({ node, messageId: 'coproc', data: { ...gate } });
43
+ }
44
+ },
45
+ };
46
+ },
47
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoDeclareGMessageId = 'declareGlobal' | 'declareGlobalWithAttributes';
7
+ /**
8
+ * The `sh/compat/no-declare-g` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noDeclareG: import("../define-shell-rule.js").ShellRule<[], NoDeclareGMessageId>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `sh/compat/no-declare-g`: reports `declare -g` and `typeset -g` in files
3
+ * whose target shell lacks the `-g` option, that is, Bash older than 4.2
4
+ * (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
5
+ *
6
+ * Bash 3.2 rejects `-g` as an invalid option, so the declaration fails and the
7
+ * variable is never created. The message suggests the portable alternative:
8
+ * in Bash before 4.2 a plain `name=value` assignment inside a function already
9
+ * sets the global, as long as the function has not declared the name `local`.
10
+ *
11
+ * What is read: `declare`/`typeset` option clusters containing `g` (`-g`,
12
+ * `-gx`, `-Ag`), found as `declarationFlagVisitor` defines. `local` and
13
+ * `export` are outside this rule's scope.
14
+ */
15
+ import { createCompatRule } from './create-compat-rule.js';
16
+ import { declarationFlagVisitor } from './declaration-options.js';
17
+ /**
18
+ * Whether a plain assignment can stand in for a command's options, which
19
+ * contain `g`: only when every option word is a `-` cluster of `g` and `x`
20
+ * letters (`-g`, `-gx`, `-g -x`), since `export name=value` expresses `x`. Any
21
+ * other option sets an attribute (`-A`, `-i`, `-r`, …) that an assignment
22
+ * cannot, whether it is joined to the `g` (`-Ag`) or a word of its own
23
+ * (`-g -A`).
24
+ */
25
+ function isPlainAssignmentEquivalent(options) {
26
+ return options.every((option) => /^-[gx]+$/u.test(option));
27
+ }
28
+ /**
29
+ * The `sh/compat/no-declare-g` rule module.
30
+ *
31
+ * @internal
32
+ */
33
+ export const noDeclareG = createCompatRule({
34
+ name: 'compat/no-declare-g',
35
+ feature: 'declareGlobal',
36
+ meta: {
37
+ type: 'problem',
38
+ docs: {
39
+ description: 'Disallow `declare -g` and `typeset -g` when the target shell lacks the option',
40
+ },
41
+ messages: {
42
+ declareGlobal: '`{{command}} -g` needs {{required}}, but this file targets {{target}}. Assign the global with a plain `name=value` (a variable not declared `local` is global), or `export name=value` to also export it.',
43
+ declareGlobalWithAttributes: '`{{command}} {{flags}}` needs {{required}}, but this file targets {{target}}. A plain `name=value` cannot express its other options, so dropping `g` is not enough: declare the variable outside any function, or raise the target.',
44
+ },
45
+ },
46
+ create(context, gate) {
47
+ return declarationFlagVisitor(context.sourceCode, 'g', (node, command, options) => {
48
+ if (!isPlainAssignmentEquivalent(options)) {
49
+ context.report({
50
+ node,
51
+ messageId: 'declareGlobalWithAttributes',
52
+ data: { ...gate, command, flags: options.join(' ') },
53
+ });
54
+ return;
55
+ }
56
+ context.report({ node, messageId: 'declareGlobal', data: { ...gate, command } });
57
+ });
58
+ },
59
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoDoubleBracketMessageId = 'doubleBracket';
7
+ /**
8
+ * The `sh/compat/no-double-bracket` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noDoubleBracket: import("../define-shell-rule.js").ShellRule<[], "doubleBracket">;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `sh/compat/no-double-bracket`: reports `[[ … ]]` in files that target POSIX
3
+ * sh. Every Bash has it, so this is a POSIX-only gate.
4
+ *
5
+ * POSIX section 2.4 leaves the results of `[[` and `]]` unspecified. dash does
6
+ * not treat `[[` as a word of its grammar: it looks up a command by that name
7
+ * and fails with `[[: not found` and status 127. Under `set -e` a bare test
8
+ * stops the script, and in an `if` condition the failure is silent in effect:
9
+ * dash takes the `else` branch. `bash --posix` and BusyBox ash accept it, so
10
+ * only some POSIX shells show the problem.
11
+ *
12
+ * What is read: a command whose effective name is `[[`, because the POSIX
13
+ * parser has no conditional clause, outside a branch for one shell (see
14
+ * `./posix-absent-command.ts`). The expression is not interpreted, and
15
+ * the parser ends the command at `&&` or `||`, so one finding is reported per
16
+ * `[[`, on that word. A `[[` the parser cannot read as a command (`=~` with a
17
+ * parenthesis, `[[ ( a ) ]]`) is a parse error and never reaches this rule.
18
+ *
19
+ * No suggestion: `[` splits and globs its unquoted operands and `==` and `=~`
20
+ * have no `[` spelling, so no rewrite keeps the behaviour in general.
21
+ */
22
+ import { createCompatRule } from './create-compat-rule.js';
23
+ import { reachableCommandWord } from './posix-absent-command.js';
24
+ /**
25
+ * The `sh/compat/no-double-bracket` rule module.
26
+ *
27
+ * @internal
28
+ */
29
+ export const noDoubleBracket = createCompatRule({
30
+ name: 'compat/no-double-bracket',
31
+ feature: 'doubleBracket',
32
+ meta: {
33
+ type: 'problem',
34
+ docs: {
35
+ description: 'Disallow `[[ … ]]` when the target shell is POSIX sh',
36
+ },
37
+ messages: {
38
+ doubleBracket: '`[[` is not part of {{target}}. dash fails with `[[: not found`, and an `if` on it takes its `else` branch. Use `[ … ]` for tests and `case` for pattern matching.',
39
+ },
40
+ },
41
+ create(context, gate) {
42
+ const { sourceCode } = context;
43
+ return {
44
+ CallExpr(node) {
45
+ const word = reachableCommandWord(sourceCode, node, '[[');
46
+ if (word === undefined)
47
+ return;
48
+ context.report({ node: word, messageId: 'doubleBracket', data: { target: gate.target } });
49
+ },
50
+ };
51
+ },
52
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoEpochVariablesMessageId = 'epochSeconds' | 'epochRealtime';
7
+ /**
8
+ * The `sh/compat/no-epoch-variables` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noEpochVariables: import("../define-shell-rule.js").ShellRule<[], NoEpochVariablesMessageId>;
@@ -0,0 +1,57 @@
1
+ import { createCompatRule } from './create-compat-rule.js';
2
+ /** The two variables, and the message each reports with. */
3
+ const EPOCH_VARIABLES = new Map([
4
+ ['EPOCHSECONDS', 'epochSeconds'],
5
+ ['EPOCHREALTIME', 'epochRealtime'],
6
+ ]);
7
+ /** The arithmetic nodes whose `Word` operands name variables. */
8
+ const ARITHMETIC_PARENTS = new Set([
9
+ 'ArithmCmd',
10
+ 'ArithmExp',
11
+ 'BinaryArithm',
12
+ 'UnaryArithm',
13
+ 'ParenArithm',
14
+ 'CStyleLoop',
15
+ ]);
16
+ /**
17
+ * The `sh/compat/no-epoch-variables` rule module.
18
+ *
19
+ * @internal
20
+ */
21
+ export const noEpochVariables = createCompatRule({
22
+ name: 'compat/no-epoch-variables',
23
+ feature: 'epochVariables',
24
+ meta: {
25
+ type: 'problem',
26
+ docs: {
27
+ description: 'Disallow `$EPOCHSECONDS` and `$EPOCHREALTIME` when the target shell lacks them',
28
+ },
29
+ messages: {
30
+ epochSeconds: '`$EPOCHSECONDS` needs {{required}}, but this file targets {{target}}, where it is unset. Use `$(date +%s)` instead (GNU, BSD and BusyBox `date`; POSIX `date` has no `%s`).',
31
+ epochRealtime: '`$EPOCHREALTIME` needs {{required}}, but this file targets {{target}}, where it is unset. `$(date +%s)` gives whole seconds (GNU, BSD and BusyBox `date`); there is no portable source of the fraction.',
32
+ },
33
+ },
34
+ create(context, gate) {
35
+ const { sourceCode } = context;
36
+ /** Reports `node` if `name` is one of the two variables. */
37
+ function check(node, name) {
38
+ const messageId = name === undefined ? undefined : EPOCH_VARIABLES.get(name);
39
+ if (messageId !== undefined)
40
+ context.report({ node, messageId, data: { ...gate } });
41
+ }
42
+ return {
43
+ ParamExp(node) {
44
+ check(node, node.param?.value);
45
+ },
46
+ Word(node) {
47
+ const parent = sourceCode.getParent(node);
48
+ if (parent === undefined || !ARITHMETIC_PARENTS.has(parent.type))
49
+ return;
50
+ const [part] = node.parts;
51
+ if (node.parts.length !== 1 || part.type !== 'Lit')
52
+ return;
53
+ check(node, part.value);
54
+ },
55
+ };
56
+ },
57
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoFdVariableMessageId = 'fdVariable';
7
+ /**
8
+ * The `sh/compat/no-fd-variable` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noFdVariable: import("../define-shell-rule.js").ShellRule<[], "fdVariable">;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `sh/compat/no-fd-variable`: reports a `{varname}` redirection, which has
3
+ * the shell pick a free file descriptor and store its number in the
4
+ * variable (`exec {fd}>file`), in files whose target shell lacks it, that is,
5
+ * Bash older than 4.1 (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
6
+ *
7
+ * Bash 4.0 and older treat `{fd}` as an ordinary word: `exec {fd}>out` tries
8
+ * to run a command named `{fd}`, which fails. The message suggests the
9
+ * portable alternative: use a fixed descriptor number, such as
10
+ * `exec 3>file`, `>&3`, then `exec 3>&-`.
11
+ *
12
+ * What is read: a `Redirect` whose descriptor is written as `{name}`, which
13
+ * the parser records separately from the redirection's target; a numbered
14
+ * descriptor (`3>file`) and a `{…}` word anywhere else (an argument, a brace
15
+ * group, quoted text) are not reported. The report is on the whole
16
+ * redirection.
17
+ */
18
+ import { createCompatRule } from './create-compat-rule.js';
19
+ /** A `{varname}` descriptor as the parser records it. */
20
+ const FD_VARIABLE = /^\{([A-Za-z_][A-Za-z0-9_]*)\}$/u;
21
+ /**
22
+ * The `sh/compat/no-fd-variable` rule module.
23
+ *
24
+ * @internal
25
+ */
26
+ export const noFdVariable = createCompatRule({
27
+ name: 'compat/no-fd-variable',
28
+ feature: 'fdVariable',
29
+ meta: {
30
+ type: 'problem',
31
+ docs: {
32
+ description: 'Disallow `{varname}` file-descriptor redirections (`exec {fd}>file`) when the target shell lacks them',
33
+ },
34
+ messages: {
35
+ fdVariable: '`{{{name}}}` descriptor allocation needs {{required}}, but this file targets {{target}}. Use a fixed descriptor number instead, for example `exec 3>file`, then `>&3`, then `exec 3>&-`.',
36
+ },
37
+ },
38
+ create(context, gate) {
39
+ return {
40
+ Redirect(node) {
41
+ const match = node.n === undefined ? null : FD_VARIABLE.exec(node.n.value);
42
+ if (match === null)
43
+ return;
44
+ context.report({ node, messageId: 'fdVariable', data: { ...gate, name: match[1] } });
45
+ },
46
+ };
47
+ },
48
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoGlobstarMessageId = 'globstar';
7
+ /**
8
+ * The `sh/compat/no-globstar` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noGlobstar: import("../define-shell-rule.js").ShellRule<[], "globstar">;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `sh/compat/no-globstar`: reports `shopt -s globstar` in files whose target
3
+ * shell lacks the option, that is, Bash older than 4.0 (macOS's stock
4
+ * `/bin/bash` is 3.2) and POSIX sh.
5
+ *
6
+ * Bash 3.2 has no such option: `shopt` prints "invalid shell option name"
7
+ * and the script carries on, and `**` then matches like `*`, so a recursive
8
+ * glob silently matches fewer files (often none). The message suggests the
9
+ * portable alternative: search the tree with `find`.
10
+ *
11
+ * What is read: a `shopt -s` command naming `globstar`, as
12
+ * `shoptEnableVisitor` defines (`shopt -u`, `shopt -o` and a name that is not
13
+ * a static word do not count). The report is on the `globstar` word.
14
+ */
15
+ import { createCompatRule } from './create-compat-rule.js';
16
+ import { shoptEnableVisitor } from './shopt-options.js';
17
+ /**
18
+ * The `sh/compat/no-globstar` rule module.
19
+ *
20
+ * @internal
21
+ */
22
+ export const noGlobstar = createCompatRule({
23
+ name: 'compat/no-globstar',
24
+ feature: 'globstar',
25
+ meta: {
26
+ type: 'problem',
27
+ docs: {
28
+ description: 'Disallow `shopt -s globstar` when the target shell lacks the option',
29
+ },
30
+ messages: {
31
+ globstar: "`shopt -s globstar` needs {{required}}, but this file targets {{target}}, where `**` matches like `*`. Search the tree with `find` instead, for example `find dir -name '*.txt'`.",
32
+ },
33
+ },
34
+ create(context, gate) {
35
+ return shoptEnableVisitor(context, 'globstar', (word) => {
36
+ context.report({ node: word, messageId: 'globstar', data: { ...gate } });
37
+ });
38
+ },
39
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoLastpipeSemanticsMessageId = 'lastpipe' | 'lastpipePosix';
7
+ /**
8
+ * The `sh/compat/no-lastpipe-semantics` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noLastpipeSemantics: import("../define-shell-rule.js").ShellRule<[], NoLastpipeSemanticsMessageId>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `sh/compat/no-lastpipe-semantics`: reports `shopt -s lastpipe` in files
3
+ * whose target shell lacks the `lastpipe` option, that is, Bash older than 4.2
4
+ * (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
5
+ *
6
+ * Bash 3.2 has no such option: `shopt` fails with "invalid shell option name"
7
+ * and the last command of every pipeline keeps running in a subshell, so a
8
+ * script that relied on `cmd | while read …; do total=…; done` leaving
9
+ * `total` set in the current shell silently loses the assignment. The message
10
+ * suggests the portable alternative: feed the loop with process substitution
11
+ * (`done < <(cmd)`), which keeps the loop in the current shell on every Bash
12
+ * that has it. A POSIX target has no process substitution, so its message
13
+ * suggests a temporary file or here-document instead.
14
+ *
15
+ * What is read: a `shopt` command (after transparent wrappers such as
16
+ * `builtin`) whose option words, up to the first operand, include `-s`
17
+ * (possibly inside a cluster such as `-sq`) and which names `lastpipe` as an
18
+ * operand. `shopt -u lastpipe` turns the option off (so does a `-u` beside the
19
+ * `-s`), `shopt -o` names `set -o` options, and a `lastpipe` that is not a static word cannot be read, so none
20
+ * of those reports.
21
+ */
22
+ import { createCompatRule } from './create-compat-rule.js';
23
+ import { shoptEnableVisitor } from './shopt-options.js';
24
+ /** The `shopt` option this rule gates. */
25
+ const LASTPIPE = 'lastpipe';
26
+ /**
27
+ * The `sh/compat/no-lastpipe-semantics` rule module.
28
+ *
29
+ * @internal
30
+ */
31
+ export const noLastpipeSemantics = createCompatRule({
32
+ name: 'compat/no-lastpipe-semantics',
33
+ feature: 'lastpipe',
34
+ meta: {
35
+ type: 'problem',
36
+ docs: {
37
+ description: 'Disallow `shopt -s lastpipe` when the target shell lacks the option',
38
+ },
39
+ messages: {
40
+ lastpipe: '`shopt -s lastpipe` needs {{required}}, but this file targets {{target}}. Keep the loop in the current shell by feeding it with process substitution instead, for example `while read -r line; do …; done < <(cmd)`.',
41
+ lastpipePosix: '`shopt -s lastpipe` needs {{required}}, but this file targets {{target}}. Process substitution is not POSIX either; keep the loop in the current shell by writing the output to a temporary file and reading that, for example `cmd > "$tmp"` then `while read -r line; do …; done < "$tmp"`, or by feeding the loop a here-document.',
42
+ },
43
+ },
44
+ create(context, gate) {
45
+ const { sourceCode } = context;
46
+ // Process substitution is a Bash feature, so a POSIX target gets advice
47
+ // that uses only POSIX constructs.
48
+ const messageId = sourceCode.dialect === 'posix' ? 'lastpipePosix' : 'lastpipe';
49
+ return shoptEnableVisitor(context, LASTPIPE, (word) => {
50
+ context.report({ node: word, messageId, data: { ...gate } });
51
+ });
52
+ },
53
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoLetMessageId = 'let';
7
+ /**
8
+ * The `sh/compat/no-let` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noLet: import("../define-shell-rule.js").ShellRule<[], "let">;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `sh/compat/no-let`: reports the `let` builtin in files that target POSIX sh.
3
+ * Every Bash has it, so this is a POSIX-only gate.
4
+ *
5
+ * POSIX section 2.9.1.4 leaves the results of a command named `let`
6
+ * unspecified. dash has no such command: `let n=1+2` fails with `let: not
7
+ * found` and status 127, and `n` stays unset. `bash --posix` and BusyBox ash
8
+ * accept it, so only some POSIX shells show the problem.
9
+ *
10
+ * What is read: a command whose effective name is `let`, because the POSIX
11
+ * parser has no `let` clause, outside a branch for one shell (see
12
+ * `./posix-absent-command.ts`).
13
+ *
14
+ * A function named `let` anywhere in the file exempts a call that names it
15
+ * directly, since dash can then run that function. This is a name check across
16
+ * the file and does not prove the function exists at the call: execution order,
17
+ * a definition in a branch or subshell, and `unset -f` are not read, so some
18
+ * exempt calls still fail on dash. A call through a wrapper is never exempt,
19
+ * because wrappers such as `command`, `env` and `exec` do not run functions.
20
+ *
21
+ * No suggestion: `let` sets its exit status from the value of its last
22
+ * expression, which `n=$((1 + 2))` does not, so the rewrite can change what
23
+ * `set -e` or an `if` sees.
24
+ */
25
+ import { definedFunctionNames } from '../../command-harvest/harvest-commands.js';
26
+ import { createCompatRule } from './create-compat-rule.js';
27
+ import { reachableCommandWord } from './posix-absent-command.js';
28
+ /** The builtin this rule gates. */
29
+ const LET = 'let';
30
+ /**
31
+ * The `sh/compat/no-let` rule module.
32
+ *
33
+ * @internal
34
+ */
35
+ export const noLet = createCompatRule({
36
+ name: 'compat/no-let',
37
+ feature: 'letCommand',
38
+ meta: {
39
+ type: 'problem',
40
+ docs: {
41
+ description: 'Disallow the `let` builtin when the target shell is POSIX sh',
42
+ },
43
+ messages: {
44
+ let: '`let` is not part of {{target}}. dash fails with `let: not found` and the assignment never happens. Use arithmetic expansion instead, for example `n=$((1 + 2))`.',
45
+ },
46
+ },
47
+ create(context, gate) {
48
+ const { sourceCode } = context;
49
+ const fileDefinesLet = definedFunctionNames(sourceCode).has(LET);
50
+ return {
51
+ CallExpr(node) {
52
+ const word = reachableCommandWord(sourceCode, node, LET);
53
+ if (word === undefined)
54
+ return;
55
+ // Only a name written directly can run a function: a wrapper skips it.
56
+ if (fileDefinesLet && sourceCode.getCommandName(node) === LET)
57
+ return;
58
+ context.report({ node: word, messageId: 'let', data: { target: gate.target } });
59
+ },
60
+ };
61
+ },
62
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Message ids this rule reports with: `mapfile` for the report and
3
+ * `useReadLoop` for its suggestion.
4
+ *
5
+ * @internal
6
+ */
7
+ export type NoMapfileMessageId = 'mapfile' | 'useReadLoop';
8
+ /**
9
+ * The `sh/compat/no-mapfile` rule module.
10
+ *
11
+ * @internal
12
+ */
13
+ export declare const noMapfile: import("../define-shell-rule.js").ShellRule<[], NoMapfileMessageId>;