@eslint-sh/eslint-plugin 0.5.0 → 0.7.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 (260) hide show
  1. package/README.md +228 -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 +51 -0
  18. package/dist/configs/posix-portable.d.ts +40 -0
  19. package/dist/configs/posix-portable.js +53 -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-brace-sequence-padding-step.d.ts +15 -0
  66. package/dist/rules/compat/no-brace-sequence-padding-step.js +287 -0
  67. package/dist/rules/compat/no-case-attributes.d.ts +12 -0
  68. package/dist/rules/compat/no-case-attributes.js +63 -0
  69. package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
  70. package/dist/rules/compat/no-case-fallthrough.js +67 -0
  71. package/dist/rules/compat/no-case-modification.d.ts +35 -0
  72. package/dist/rules/compat/no-case-modification.js +86 -0
  73. package/dist/rules/compat/no-coproc.d.ts +12 -0
  74. package/dist/rules/compat/no-coproc.js +47 -0
  75. package/dist/rules/compat/no-declare-g.d.ts +12 -0
  76. package/dist/rules/compat/no-declare-g.js +59 -0
  77. package/dist/rules/compat/no-double-bracket.d.ts +12 -0
  78. package/dist/rules/compat/no-double-bracket.js +52 -0
  79. package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
  80. package/dist/rules/compat/no-epoch-variables.js +57 -0
  81. package/dist/rules/compat/no-fd-variable.d.ts +12 -0
  82. package/dist/rules/compat/no-fd-variable.js +48 -0
  83. package/dist/rules/compat/no-globstar.d.ts +12 -0
  84. package/dist/rules/compat/no-globstar.js +39 -0
  85. package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
  86. package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
  87. package/dist/rules/compat/no-let.d.ts +12 -0
  88. package/dist/rules/compat/no-let.js +62 -0
  89. package/dist/rules/compat/no-mapfile.d.ts +13 -0
  90. package/dist/rules/compat/no-mapfile.js +122 -0
  91. package/dist/rules/compat/no-nameref.d.ts +12 -0
  92. package/dist/rules/compat/no-nameref.js +44 -0
  93. package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
  94. package/dist/rules/compat/no-negative-array-index.js +123 -0
  95. package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
  96. package/dist/rules/compat/no-nounset-empty-array.js +158 -0
  97. package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
  98. package/dist/rules/compat/no-parameter-transformation.js +123 -0
  99. package/dist/rules/compat/no-pipe-all.d.ts +13 -0
  100. package/dist/rules/compat/no-pipe-all.js +68 -0
  101. package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
  102. package/dist/rules/compat/no-printf-time-format.js +99 -0
  103. package/dist/rules/compat/no-source.d.ts +13 -0
  104. package/dist/rules/compat/no-source.js +103 -0
  105. package/dist/rules/compat/no-test-v.d.ts +12 -0
  106. package/dist/rules/compat/no-test-v.js +280 -0
  107. package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
  108. package/dist/rules/compat/no-unicode-escape.js +120 -0
  109. package/dist/rules/compat/no-wait-n.d.ts +12 -0
  110. package/dist/rules/compat/no-wait-n.js +73 -0
  111. package/dist/rules/compat/operator-range.d.ts +10 -0
  112. package/dist/rules/compat/operator-range.js +15 -0
  113. package/dist/rules/compat/posix-absent-command.d.ts +12 -0
  114. package/dist/rules/compat/posix-absent-command.js +134 -0
  115. package/dist/rules/compat/shopt-options.d.ts +9 -0
  116. package/dist/rules/compat/shopt-options.js +63 -0
  117. package/dist/rules/compat/version-gates.d.ts +76 -0
  118. package/dist/rules/compat/version-gates.js +205 -0
  119. package/dist/rules/create-rule.d.ts +91 -0
  120. package/dist/rules/create-rule.js +51 -0
  121. package/dist/rules/create-wrapper-rule.d.ts +166 -0
  122. package/dist/rules/create-wrapper-rule.js +407 -0
  123. package/dist/rules/define-shell-rule.d.ts +184 -0
  124. package/dist/rules/define-shell-rule.js +56 -0
  125. package/dist/rules/index.d.ts +26 -0
  126. package/dist/rules/index.js +90 -0
  127. package/dist/rules/max-script-complexity.d.ts +28 -0
  128. package/dist/rules/max-script-complexity.js +130 -0
  129. package/dist/rules/no-dynamic-invocation.d.ts +35 -0
  130. package/dist/rules/no-dynamic-invocation.js +117 -0
  131. package/dist/rules/no-eval.d.ts +13 -0
  132. package/dist/rules/no-eval.js +89 -0
  133. package/dist/rules/no-masked-exit-status.d.ts +21 -0
  134. package/dist/rules/no-masked-exit-status.js +667 -0
  135. package/dist/rules/no-secret-env-reads.d.ts +24 -0
  136. package/dist/rules/no-secret-env-reads.js +133 -0
  137. package/dist/rules/no-secret-flags.d.ts +32 -0
  138. package/dist/rules/no-secret-flags.js +162 -0
  139. package/dist/rules/no-unchecked-cd.d.ts +44 -0
  140. package/dist/rules/no-unchecked-cd.js +250 -0
  141. package/dist/rules/no-undeclared-commands.d.ts +20 -0
  142. package/dist/rules/no-undeclared-commands.js +115 -0
  143. package/dist/rules/no-unused-declarations.d.ts +23 -0
  144. package/dist/rules/no-unused-declarations.js +259 -0
  145. package/dist/rules/prefer-local.d.ts +16 -0
  146. package/dist/rules/prefer-local.js +249 -0
  147. package/dist/rules/quote-expansions.d.ts +13 -0
  148. package/dist/rules/quote-expansions.js +229 -0
  149. package/dist/rules/require-network-wrapper.d.ts +40 -0
  150. package/dist/rules/require-network-wrapper.js +244 -0
  151. package/dist/rules/require-shebang.d.ts +29 -0
  152. package/dist/rules/require-shebang.js +101 -0
  153. package/dist/rules/require-strict-mode.d.ts +52 -0
  154. package/dist/rules/require-strict-mode.js +177 -0
  155. package/dist/rules/require-tty-guard.d.ts +13 -0
  156. package/dist/rules/require-tty-guard.js +318 -0
  157. package/dist/rules/resolve-declaration-convention.d.ts +96 -0
  158. package/dist/rules/resolve-declaration-convention.js +109 -0
  159. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  160. package/dist/rules/resolve-network-wrapper.js +116 -0
  161. package/dist/rules/set-options.d.ts +109 -0
  162. package/dist/rules/set-options.js +201 -0
  163. package/dist/rules/shebang-interpreter.d.ts +18 -0
  164. package/dist/rules/shebang-interpreter.js +81 -0
  165. package/dist/rules/shebang-matches-dialect.d.ts +12 -0
  166. package/dist/rules/shebang-matches-dialect.js +69 -0
  167. package/dist/rules/transparent-wrappers.d.ts +52 -0
  168. package/dist/rules/transparent-wrappers.js +99 -0
  169. package/dist/rules/user-pattern.d.ts +63 -0
  170. package/dist/rules/user-pattern.js +184 -0
  171. package/dist/syntax/analyze.d.ts +19 -0
  172. package/dist/syntax/analyze.js +21 -0
  173. package/dist/syntax/index.d.ts +23 -0
  174. package/dist/syntax/index.js +25 -0
  175. package/dist/version.d.ts +0 -1
  176. package/dist/version.js +0 -1
  177. package/docs/compat.md +354 -0
  178. package/docs/custom-rule-tutorial.md +780 -34
  179. package/docs/declaration-conventions.md +478 -0
  180. package/docs/examples/audit-wrapper-pack.mjs +15 -0
  181. package/docs/examples/dep-line-convention.mjs +22 -0
  182. package/docs/examples/eslint.config.mjs +3 -2
  183. package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
  184. package/docs/examples/no-backticks-rule-tester.mjs +106 -0
  185. package/docs/examples/no-backticks-rule.mjs +60 -0
  186. package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
  187. package/docs/examples/no-function-keyword-rule.mjs +59 -0
  188. package/docs/examples/no-listed-commands-rule.ts +63 -0
  189. package/docs/examples/no-rm-rf-rule.mjs +26 -15
  190. package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
  191. package/docs/examples/sidecar-convention.mjs +38 -0
  192. package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
  193. package/docs/examples/sorted-declarations-rule.mjs +52 -0
  194. package/docs/reading-nodes.md +159 -0
  195. package/docs/rules/README.md +79 -0
  196. package/docs/rules/_template.md +83 -0
  197. package/docs/rules/compat/no-append-all-redirect.md +87 -0
  198. package/docs/rules/compat/no-associative-arrays.md +95 -0
  199. package/docs/rules/compat/no-brace-sequence-padding-step.md +90 -0
  200. package/docs/rules/compat/no-case-attributes.md +82 -0
  201. package/docs/rules/compat/no-case-fallthrough.md +90 -0
  202. package/docs/rules/compat/no-case-modification.md +74 -0
  203. package/docs/rules/compat/no-coproc.md +76 -0
  204. package/docs/rules/compat/no-declare-g.md +80 -0
  205. package/docs/rules/compat/no-double-bracket.md +72 -0
  206. package/docs/rules/compat/no-epoch-variables.md +79 -0
  207. package/docs/rules/compat/no-fd-variable.md +78 -0
  208. package/docs/rules/compat/no-globstar.md +74 -0
  209. package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
  210. package/docs/rules/compat/no-let.md +78 -0
  211. package/docs/rules/compat/no-mapfile.md +118 -0
  212. package/docs/rules/compat/no-nameref.md +81 -0
  213. package/docs/rules/compat/no-negative-array-index.md +87 -0
  214. package/docs/rules/compat/no-nounset-empty-array.md +103 -0
  215. package/docs/rules/compat/no-parameter-transformation.md +94 -0
  216. package/docs/rules/compat/no-pipe-all.md +85 -0
  217. package/docs/rules/compat/no-printf-time-format.md +75 -0
  218. package/docs/rules/compat/no-source.md +98 -0
  219. package/docs/rules/compat/no-test-v.md +82 -0
  220. package/docs/rules/compat/no-unicode-escape.md +91 -0
  221. package/docs/rules/compat/no-wait-n.md +85 -0
  222. package/docs/rules/max-script-complexity.md +109 -0
  223. package/docs/rules/no-dynamic-invocation.md +139 -0
  224. package/docs/rules/no-eval.md +106 -0
  225. package/docs/rules/no-masked-exit-status.md +239 -0
  226. package/docs/rules/no-secret-env-reads.md +173 -0
  227. package/docs/rules/no-secret-flags.md +169 -0
  228. package/docs/rules/no-unchecked-cd.md +146 -0
  229. package/docs/rules/no-undeclared-commands.md +141 -0
  230. package/docs/rules/no-unused-declarations.md +216 -0
  231. package/docs/rules/prefer-local.md +221 -0
  232. package/docs/rules/quote-expansions.md +157 -0
  233. package/docs/rules/require-network-wrapper.md +171 -0
  234. package/docs/rules/require-shebang.md +157 -0
  235. package/docs/rules/require-strict-mode.md +253 -0
  236. package/docs/rules/require-tty-guard.md +131 -0
  237. package/docs/rules/shebang-matches-dialect.md +120 -0
  238. package/docs/selectors.md +2 -2
  239. package/docs/shellcheck.md +87 -0
  240. package/docs/untrusted-code.md +440 -0
  241. package/docs/wrapper-rules.md +206 -0
  242. package/package.json +26 -20
  243. package/dist/index.d.ts.map +0 -1
  244. package/dist/index.js.map +0 -1
  245. package/dist/language/selector-classes.d.ts.map +0 -1
  246. package/dist/language/selector-classes.js.map +0 -1
  247. package/dist/language/shell-language-options.d.ts.map +0 -1
  248. package/dist/language/shell-language-options.js.map +0 -1
  249. package/dist/language/shell-language.d.ts.map +0 -1
  250. package/dist/language/shell-language.js.map +0 -1
  251. package/dist/language/shell-source-code.d.ts.map +0 -1
  252. package/dist/language/shell-source-code.js.map +0 -1
  253. package/dist/language/source-coordinates.d.ts.map +0 -1
  254. package/dist/language/source-coordinates.js.map +0 -1
  255. package/dist/language/syntax-view.d.ts.map +0 -1
  256. package/dist/language/syntax-view.js.map +0 -1
  257. package/dist/language/word-resolution.d.ts.map +0 -1
  258. package/dist/language/word-resolution.js.map +0 -1
  259. package/dist/version.d.ts.map +0 -1
  260. package/dist/version.js.map +0 -1
@@ -0,0 +1,103 @@
1
+ /**
2
+ * `sh/compat/no-source`: reports the `source` builtin in files that target
3
+ * POSIX sh, where the portable name is the `.` special built-in. Every Bash has
4
+ * `source`, so this is a POSIX-only gate.
5
+ *
6
+ * POSIX section 2.9.1.4 leaves the results of a command named `source`
7
+ * unspecified. dash has no such command: `source ./lib.sh` fails with `source:
8
+ * not found` and status 127, and the file is never read. `bash --posix` and
9
+ * BusyBox ash accept it, so only some POSIX shells show the problem.
10
+ *
11
+ * What is read: a command whose effective name is `source`, outside a branch
12
+ * for one shell (see `./posix-absent-command.ts`).
13
+ *
14
+ * A function named `source` anywhere in the file exempts a call that names it
15
+ * directly, since dash can then run that function; `bash --posix` refuses that
16
+ * definition, which this rule does not judge. This is a name check across the
17
+ * file and does not prove the function exists at the call: execution order, a
18
+ * definition in a branch or subshell, and `unset -f` are not read, so some exempt
19
+ * calls still fail on dash. A call through a wrapper is never exempt, because
20
+ * wrappers such as `command`, `env` and `exec` do not run functions.
21
+ *
22
+ * The suggestion replaces the word with `.` only where the two read the same
23
+ * file with the same effect, apart from a file that cannot be read: `.` then
24
+ * ends the script on a POSIX shell, so a `|| fallback` never runs, and the
25
+ * suggestion's text says so. The conditions are:
26
+ *
27
+ * - The command is written directly, with no wrapper.
28
+ * - It has no leading `VAR=value`. Such an assignment persists after `.` on a
29
+ * POSIX shell, while Bash outside POSIX mode scopes it to the `source`
30
+ * command.
31
+ * - It has exactly one operand. `.` takes one in POSIX, and dash ignores any
32
+ * others where `source` would set the positional parameters.
33
+ * - The operand has a literal `/`. A name without one makes `.` search `PATH`.
34
+ */
35
+ import { definedFunctionNames } from '../../command-harvest/harvest-commands.js';
36
+ import { childNodes } from '../../language/syntax-view.js';
37
+ import { createCompatRule } from './create-compat-rule.js';
38
+ import { reachableCommandWord } from './posix-absent-command.js';
39
+ /** The builtin this rule gates. */
40
+ const SOURCE = 'source';
41
+ /**
42
+ * Whether `word` holds a `/` that no expansion can remove: in unquoted or
43
+ * quoted literal text, whatever its other parts expand to. A `/` inside an
44
+ * expansion (`${x%/*}`, `$(pwd)`) does not count, so the walk stops at anything
45
+ * that is not literal text or a double-quoted group.
46
+ */
47
+ function hasLiteralSlash(word) {
48
+ return childNodes(word).some((part) => {
49
+ if (part.type === 'Lit' || part.type === 'SglQuoted')
50
+ return part.value.includes('/');
51
+ return part.type === 'DblQuoted' && hasLiteralSlash(part);
52
+ });
53
+ }
54
+ /** Whether `.` is an exact replacement for this `source` command (see the module comment). */
55
+ function dotIsEquivalent(sourceCode, node) {
56
+ if (sourceCode.getCommandName(node) !== SOURCE)
57
+ return false;
58
+ if (node.assigns.length > 0 || node.args.length !== 2)
59
+ return false;
60
+ return hasLiteralSlash(node.args[1]);
61
+ }
62
+ /**
63
+ * The `sh/compat/no-source` rule module.
64
+ *
65
+ * @internal
66
+ */
67
+ export const noSource = createCompatRule({
68
+ name: 'compat/no-source',
69
+ feature: 'sourceCommand',
70
+ meta: {
71
+ type: 'problem',
72
+ hasSuggestions: true,
73
+ docs: {
74
+ description: 'Disallow the `source` builtin when the target shell is POSIX sh',
75
+ },
76
+ messages: {
77
+ source: '`source` is not part of {{target}}. dash fails with `source: not found` and the file is never read. Use the `.` special built-in with a path that contains a slash, for example `. ./lib.sh`.',
78
+ useDot: 'Replace `source` with `.`, which ends the script if the file is missing.',
79
+ },
80
+ },
81
+ create(context, gate) {
82
+ const { sourceCode } = context;
83
+ const fileDefinesSource = definedFunctionNames(sourceCode).has(SOURCE);
84
+ return {
85
+ CallExpr(node) {
86
+ const word = reachableCommandWord(sourceCode, node, SOURCE);
87
+ if (word === undefined)
88
+ return;
89
+ // Only a name written directly can run a function: a wrapper skips it.
90
+ if (fileDefinesSource && sourceCode.getCommandName(node) === SOURCE)
91
+ return;
92
+ context.report({
93
+ node: word,
94
+ messageId: 'source',
95
+ data: { target: gate.target },
96
+ suggest: dotIsEquivalent(sourceCode, node)
97
+ ? [{ messageId: 'useDot', fix: (fixer) => fixer.replaceText(word, '.') }]
98
+ : [],
99
+ });
100
+ },
101
+ };
102
+ },
103
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoTestVMessageId = 'testV';
7
+ /**
8
+ * The `sh/compat/no-test-v` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noTestV: import("../define-shell-rule.js").ShellRule<[], "testV">;
@@ -0,0 +1,280 @@
1
+ /**
2
+ * `sh/compat/no-test-v`: reports the `-v name` test (`[[ -v x ]]`,
3
+ * `test -v x`, `[ -v x ]`) in files whose target shell lacks it, that is,
4
+ * Bash older than 4.2 (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
5
+ *
6
+ * In Bash 4.1 and older, `[[ -v x ]]` is a syntax error, so the whole script
7
+ * fails to parse, and `test -v x` fails with "unary operator expected". The
8
+ * message suggests the portable alternative, `[ -n "${x+set}" ]`, which is
9
+ * true exactly when `x` is set (even to the empty string) and is safe under
10
+ * `set -u`.
11
+ *
12
+ * What is read:
13
+ *
14
+ * - inside `[[ … ]]`, the parser's `-v` unary test node;
15
+ * - for `test` and `[` (after transparent wrappers such as `builtin`), the
16
+ * arguments, read the way Bash's `test` reads them: with two arguments a
17
+ * leading `-v` is the operator; with three, the middle one is a binary
18
+ * operator if it can be one, and `! -v name` negates the test; with four,
19
+ * `! …` and `( … )` peel off; anything longer goes through `test`'s own
20
+ * precedence parser, where `-v` is the operator only where a primary is
21
+ * expected and another argument follows it. So `[ "$x" = -v ]` and
22
+ * `[ -n -v ]` do not report. A double-quoted expansion (`"$name"`) is one
23
+ * operand whatever its value. Any other argument that is not a static word
24
+ * ends the reading, because it may expand to several arguments or none.
25
+ *
26
+ * The report is on the `[[` test, or on the `-v` word.
27
+ */
28
+ import { TsVarSet } from 'sh-ast';
29
+ import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
30
+ import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
31
+ import { createCompatRule } from './create-compat-rule.js';
32
+ /**
33
+ * The wrappers followed to find the `test` command: `sh-ast/analyze`'s
34
+ * defaults (which include `command`) plus Bash's `builtin`.
35
+ */
36
+ const TRANSPARENT_WRAPPERS = [
37
+ ...DEFAULT_TRANSPARENT_WRAPPERS,
38
+ BUILTIN_WRAPPER,
39
+ ];
40
+ /** The commands that are Bash's `test` builtin. */
41
+ const TEST_COMMANDS = new Set(['test', '[']);
42
+ /** Bash's `test` binary operators (`test_binop`), without `-a` and `-o`. */
43
+ const BINARY_OPERATORS = new Set([
44
+ '=',
45
+ '==',
46
+ '!=',
47
+ '<',
48
+ '>',
49
+ '-eq',
50
+ '-ne',
51
+ '-lt',
52
+ '-le',
53
+ '-gt',
54
+ '-ge',
55
+ '-nt',
56
+ '-ot',
57
+ '-ef',
58
+ ]);
59
+ /** Bash's `test` unary operators (`test_unop`). */
60
+ const UNARY_OPERATORS = new Set([
61
+ '-a',
62
+ '-b',
63
+ '-c',
64
+ '-d',
65
+ '-e',
66
+ '-f',
67
+ '-g',
68
+ '-h',
69
+ '-k',
70
+ '-n',
71
+ '-o',
72
+ '-p',
73
+ '-r',
74
+ '-s',
75
+ '-t',
76
+ '-u',
77
+ '-v',
78
+ '-w',
79
+ '-x',
80
+ '-z',
81
+ '-G',
82
+ '-L',
83
+ '-O',
84
+ '-S',
85
+ '-N',
86
+ '-R',
87
+ ]);
88
+ /**
89
+ * What a double-quoted, non-static argument stands for while reading `test`'s
90
+ * arguments: an operand that is never an operator. A NUL cannot occur in a
91
+ * shell word, so no real argument equals it.
92
+ */
93
+ const QUOTED_OPERAND = '\0operand';
94
+ /** A shell variable name, optionally with an array subscript. */
95
+ const VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(?:\[[^\]]*\])?$/u;
96
+ /**
97
+ * The indices of the arguments in `args` that Bash's `test` reads as the
98
+ * `-v` operator. `args` are the static texts after the command name (and
99
+ * without a closing `]`); reading stops at the first `null`.
100
+ */
101
+ function variableTestIndices(args) {
102
+ const end = args.indexOf(null);
103
+ const argv = (end === -1 ? args : args.slice(0, end));
104
+ const found = [];
105
+ /** `test`'s two-argument rule, at offset `at`. */
106
+ function twoArguments(at) {
107
+ if (argv[at] === '-v')
108
+ found.push(at);
109
+ }
110
+ /** `test`'s three-argument rule, at offset `at`. */
111
+ function threeArguments(at) {
112
+ const middle = argv[at + 1];
113
+ if (BINARY_OPERATORS.has(middle) || middle === '-a' || middle === '-o')
114
+ return true;
115
+ if (argv[at] === '!') {
116
+ twoArguments(at + 1);
117
+ return true;
118
+ }
119
+ return argv[at] === '(' && argv[at + 2] === ')';
120
+ }
121
+ /** `test`'s precedence parser over every argument (`expr` in Bash's test.c). */
122
+ function expression() {
123
+ let pos = 0;
124
+ const term = () => {
125
+ if (pos >= argv.length)
126
+ return;
127
+ const word = argv[pos];
128
+ if (word === '!') {
129
+ pos++;
130
+ term();
131
+ return;
132
+ }
133
+ if (word === '(') {
134
+ pos++;
135
+ or();
136
+ if (argv[pos] === ')')
137
+ pos++;
138
+ return;
139
+ }
140
+ if (pos + 3 <= argv.length && BINARY_OPERATORS.has(argv[pos + 1])) {
141
+ pos += 3;
142
+ return;
143
+ }
144
+ if (UNARY_OPERATORS.has(word) && pos + 1 < argv.length) {
145
+ if (word === '-v')
146
+ found.push(pos);
147
+ pos += 2;
148
+ return;
149
+ }
150
+ pos++;
151
+ };
152
+ const and = () => {
153
+ term();
154
+ while (argv[pos] === '-a') {
155
+ pos++;
156
+ term();
157
+ }
158
+ };
159
+ const or = () => {
160
+ and();
161
+ while (argv[pos] === '-o') {
162
+ pos++;
163
+ and();
164
+ }
165
+ };
166
+ while (pos < argv.length) {
167
+ const before = pos;
168
+ or();
169
+ if (pos === before)
170
+ pos++;
171
+ }
172
+ }
173
+ switch (argv.length) {
174
+ case 0:
175
+ case 1:
176
+ break;
177
+ case 2:
178
+ if (argv[0] !== '!')
179
+ twoArguments(0);
180
+ break;
181
+ case 3:
182
+ if (!threeArguments(0))
183
+ expression();
184
+ break;
185
+ case 4:
186
+ if (argv[0] === '!') {
187
+ if (!threeArguments(1))
188
+ expression();
189
+ }
190
+ else if (argv[0] === '(' && argv[3] === ')') {
191
+ if (argv[1] !== '!')
192
+ twoArguments(1);
193
+ }
194
+ else {
195
+ expression();
196
+ }
197
+ break;
198
+ default:
199
+ expression();
200
+ }
201
+ return found;
202
+ }
203
+ /**
204
+ * The `sh/compat/no-test-v` rule module.
205
+ *
206
+ * @internal
207
+ */
208
+ export const noTestV = createCompatRule({
209
+ name: 'compat/no-test-v',
210
+ feature: 'testVariableSet',
211
+ meta: {
212
+ type: 'problem',
213
+ docs: {
214
+ description: 'Disallow the `-v name` test (`[[ -v x ]]`, `test -v x`) when the target shell lacks it',
215
+ },
216
+ messages: {
217
+ testV: '`-v {{name}}` needs {{required}}, but this file targets {{target}}. Test whether the variable is set with `[ -n "${{{name}}+set}" ]` instead.',
218
+ },
219
+ },
220
+ create(context, gate) {
221
+ const { sourceCode } = context;
222
+ /** The variable name shown in the message for `operand`. */
223
+ function nameOf(operand) {
224
+ const text = operand?.type === 'Word' ? sourceCode.resolveWordText(operand) : null;
225
+ return text !== null && VARIABLE_NAME.test(text) ? text : 'name';
226
+ }
227
+ /**
228
+ * The argument `word` gives `test`: its static text; a stand-in operand
229
+ * for a word that is one double-quoted string, which always makes exactly
230
+ * one argument whatever it expands to; or `null` for any other word, which
231
+ * may split into several arguments or none.
232
+ */
233
+ function argumentText(word) {
234
+ const text = sourceCode.resolveWordText(word);
235
+ if (text !== null)
236
+ return text;
237
+ if (word.parts.length === 1 && word.parts[0].type === 'DblQuoted') {
238
+ return QUOTED_OPERAND;
239
+ }
240
+ return null;
241
+ }
242
+ return {
243
+ UnaryTest(node) {
244
+ if (node.op !== TsVarSet)
245
+ return;
246
+ context.report({
247
+ node,
248
+ messageId: 'testV',
249
+ data: { ...gate, name: nameOf(node.x) },
250
+ });
251
+ },
252
+ CallExpr(node) {
253
+ const invoked = sourceCode.resolveInvokedCommand(node, {
254
+ transparentWrappers: TRANSPARENT_WRAPPERS,
255
+ });
256
+ if (invoked?.effective.static !== true)
257
+ return;
258
+ const command = invoked.effective.text;
259
+ if (!TEST_COMMANDS.has(command))
260
+ return;
261
+ // `chainIndices` index `node.args` directly, so slice before filtering.
262
+ let words = node.args.slice((invoked.chainIndices.at(-1) ?? 0) + 1);
263
+ if (command === '[') {
264
+ const last = words.at(-1);
265
+ if (last === undefined || sourceCode.resolveWordText(last) !== ']')
266
+ return;
267
+ words = words.slice(0, -1);
268
+ }
269
+ const texts = words.map((word) => argumentText(word));
270
+ for (const index of variableTestIndices(texts)) {
271
+ context.report({
272
+ node: words[index],
273
+ messageId: 'testV',
274
+ data: { ...gate, name: nameOf(words[index + 1]) },
275
+ });
276
+ }
277
+ },
278
+ };
279
+ },
280
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Message ids this rule reports with: `unicodeEscape` when the target leaves
3
+ * the escape undecoded, `unicodeEscapeLatin1` when it decodes it to a
4
+ * Latin-1 byte.
5
+ *
6
+ * @internal
7
+ */
8
+ export type NoUnicodeEscapeMessageId = 'unicodeEscape' | 'unicodeEscapeLatin1';
9
+ /**
10
+ * The `sh/compat/no-unicode-escape` rule module.
11
+ *
12
+ * @internal
13
+ */
14
+ export declare const noUnicodeEscape: import("../define-shell-rule.js").ShellRule<[], NoUnicodeEscapeMessageId>;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `sh/compat/no-unicode-escape`: reports the `\uXXXX` and `\UXXXXXXXX`
3
+ * escapes in a `$'…'` string in files whose target shell does not decode
4
+ * them to UTF-8. The minimum Bash depends on the code point:
5
+ *
6
+ * - Bash 4.1 and older leave every such escape as literal text, with no
7
+ * error: Bash 4.2 is the first release that decodes them;
8
+ * - the Bash 4.2 release writes a code point from U+0080 to U+00FF as a
9
+ * single Latin-1 byte even in a UTF-8 locale (observed: `e9` for U+00E9,
10
+ * where UTF-8 is `c3 a9`), while every other code point is UTF-8. Bash 4.3
11
+ * is the first release that writes them all as UTF-8.
12
+ *
13
+ * The message suggests the portable alternative: write the character's
14
+ * UTF-8 bytes as octal escapes, which every Bash decodes in `$'…'` (for
15
+ * example `$'\303\251'` for U+00E9). Under `dialect: "posix"` the parser
16
+ * reads `$'…'` as a literal `$` followed by an ordinary single-quoted string,
17
+ * as dash does, so there is no `$'…'` string to report; the gate's
18
+ * `posix: false` records that POSIX sh does not decode these escapes.
19
+ *
20
+ * What is read: a `$'…'` string's text, scanned as Bash decodes it: `\\` is a
21
+ * backslash, `\cX` is one control character (so in `\cu00e9` the `u`
22
+ * belongs to `\c`), and `\u` takes one to four hex digits, `\U` one to
23
+ * eight. A `\u` with no hex digit after it is literal text. Plain `'…'`
24
+ * and `"…"` strings do not decode escapes and are not read. The report is on
25
+ * the whole string, once per string.
26
+ */
27
+ import { createMultiFeatureCompatRule } from './create-compat-rule.js';
28
+ /** The backslash that starts every escape. */
29
+ const BACKSLASH = '\\';
30
+ /** The unicode escapes of a `$'…'` string's text, in order. */
31
+ function unicodeEscapes(text) {
32
+ const found = [];
33
+ let index = 0;
34
+ while (index < text.length) {
35
+ if (text[index] !== BACKSLASH) {
36
+ index++;
37
+ continue;
38
+ }
39
+ const letter = text[index + 1];
40
+ if (letter === 'c') {
41
+ index += 3;
42
+ continue;
43
+ }
44
+ const maxDigits = letter === 'u' ? 4 : letter === 'U' ? 8 : 0;
45
+ if (maxDigits === 0) {
46
+ index += 2;
47
+ continue;
48
+ }
49
+ const digits = /^[0-9A-Fa-f]+/u.exec(text.slice(index + 2, index + 2 + maxDigits))?.[0];
50
+ if (digits === undefined) {
51
+ index += 2;
52
+ continue;
53
+ }
54
+ const length = 2 + digits.length;
55
+ found.push({ text: text.slice(index, index + length), codePoint: parseInt(digits, 16) });
56
+ index += length;
57
+ }
58
+ return found;
59
+ }
60
+ /** Whether a code point is one the Bash 4.2 release writes as a Latin-1 byte. */
61
+ function isLatin1Upper(codePoint) {
62
+ return codePoint >= 0x80 && codePoint <= 0xff;
63
+ }
64
+ /**
65
+ * The UTF-8 bytes of `codePoint` as octal escapes (`\303\251`), or
66
+ * `undefined` for a value that is not a Unicode scalar value.
67
+ */
68
+ function octalBytes(codePoint) {
69
+ if (codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff))
70
+ return undefined;
71
+ const bytes = new TextEncoder().encode(String.fromCodePoint(codePoint));
72
+ return [...bytes].map((byte) => BACKSLASH + byte.toString(8).padStart(3, '0')).join('');
73
+ }
74
+ /**
75
+ * The `sh/compat/no-unicode-escape` rule module.
76
+ *
77
+ * @internal
78
+ */
79
+ export const noUnicodeEscape = createMultiFeatureCompatRule({
80
+ name: 'compat/no-unicode-escape',
81
+ features: ['unicodeEscape', 'unicodeEscapeLatin1'],
82
+ meta: {
83
+ type: 'problem',
84
+ docs: {
85
+ description: "Disallow `\\u`/`\\U` escapes in `$'…'` strings when the target shell does not decode them to UTF-8",
86
+ },
87
+ messages: {
88
+ unicodeEscape: "`{{escape}}` in `$'…'` needs {{required}}, but this file targets {{target}}, which leaves it as literal text. Write the UTF-8 bytes as octal escapes instead, for example `$'{{octal}}'`.",
89
+ unicodeEscapeLatin1: "`{{escape}}` in `$'…'` needs {{required}}, but this file targets {{target}}, which writes U+0080 to U+00FF as a single Latin-1 byte, not UTF-8. Write the UTF-8 bytes as octal escapes instead, for example `$'{{octal}}'`.",
90
+ },
91
+ },
92
+ create(context, gates) {
93
+ const escapeGate = gates.get('unicodeEscape');
94
+ const latin1Gate = gates.get('unicodeEscapeLatin1');
95
+ return {
96
+ SglQuoted(node) {
97
+ if (!node.dollar)
98
+ return;
99
+ const escapes = unicodeEscapes(node.value);
100
+ const latin1 = escapes.find((escape) => isLatin1Upper(escape.codePoint));
101
+ // The escape the message names, and the gate it needs: a Latin-1
102
+ // escape needs the newer release, so it decides when present.
103
+ const shown = latin1 ?? escapes.at(0);
104
+ const gate = latin1 === undefined ? escapeGate : latin1Gate;
105
+ if (shown === undefined || gate === undefined)
106
+ return;
107
+ const messageId = escapeGate === undefined ? 'unicodeEscapeLatin1' : 'unicodeEscape';
108
+ context.report({
109
+ node,
110
+ messageId,
111
+ data: {
112
+ ...gate,
113
+ escape: shown.text,
114
+ octal: octalBytes(shown.codePoint) ?? '\\NNN',
115
+ },
116
+ });
117
+ },
118
+ };
119
+ },
120
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoWaitNMessageId = 'waitNext';
7
+ /**
8
+ * The `sh/compat/no-wait-n` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noWaitN: import("../define-shell-rule.js").ShellRule<[], "waitNext">;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `sh/compat/no-wait-n`: reports `wait -n` in files whose target shell lacks
3
+ * it, that is, Bash older than 4.3 (macOS's stock `/bin/bash` is 3.2) and
4
+ * POSIX sh.
5
+ *
6
+ * Bash 4.2 and older reject `-n` as an invalid option and return 2 at once,
7
+ * so a job pool that throttles itself with `wait -n` stops waiting and runs
8
+ * every job together. The message suggests the portable alternative: keep
9
+ * each job's `$!` and wait for one job at a time with `wait "$pid"`, which
10
+ * returns that job's exit status.
11
+ *
12
+ * What is read: a `wait` command (after transparent wrappers such as
13
+ * `builtin`) whose option words, up to the first operand or `--`, include a
14
+ * cluster containing `n` (`-n`, `-fn`). The value after a `-p` cluster
15
+ * (Bash 5.1's `wait -p var`) is its operand, not an option. The report is on
16
+ * the option word.
17
+ */
18
+ import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
19
+ import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
20
+ import { createCompatRule } from './create-compat-rule.js';
21
+ /**
22
+ * The wrappers followed to find the `wait` command: `sh-ast/analyze`'s
23
+ * defaults (which include `command`) plus Bash's `builtin`.
24
+ */
25
+ const TRANSPARENT_WRAPPERS = [
26
+ ...DEFAULT_TRANSPARENT_WRAPPERS,
27
+ BUILTIN_WRAPPER,
28
+ ];
29
+ /** An option cluster of `wait`: letters after a single `-`. */
30
+ const OPTION_CLUSTER = /^-[A-Za-z]+$/u;
31
+ /**
32
+ * The `sh/compat/no-wait-n` rule module.
33
+ *
34
+ * @internal
35
+ */
36
+ export const noWaitN = createCompatRule({
37
+ name: 'compat/no-wait-n',
38
+ feature: 'waitNext',
39
+ meta: {
40
+ type: 'problem',
41
+ docs: {
42
+ description: 'Disallow `wait -n` when the target shell lacks it',
43
+ },
44
+ messages: {
45
+ waitNext: '`wait -n` needs {{required}}, but this file targets {{target}}. Wait for one job at a time instead: keep each job\'s `$!` and run `wait "$pid"`, which returns that job\'s exit status.',
46
+ },
47
+ },
48
+ create(context, gate) {
49
+ const { sourceCode } = context;
50
+ return {
51
+ CallExpr(node) {
52
+ const invoked = sourceCode.resolveInvokedCommand(node, {
53
+ transparentWrappers: TRANSPARENT_WRAPPERS,
54
+ });
55
+ if (invoked?.effective.static !== true || invoked.effective.text !== 'wait')
56
+ return;
57
+ // `chainIndices` index `node.args` directly, so slice before filtering.
58
+ const words = node.args.slice((invoked.chainIndices.at(-1) ?? 0) + 1);
59
+ for (let index = 0; index < words.length; index++) {
60
+ const text = sourceCode.resolveWordText(words[index]);
61
+ if (text === null || !OPTION_CLUSTER.test(text))
62
+ return;
63
+ if (text.includes('n')) {
64
+ context.report({ node: words[index], messageId: 'waitNext', data: { ...gate } });
65
+ return;
66
+ }
67
+ if (text.includes('p'))
68
+ index++;
69
+ }
70
+ },
71
+ };
72
+ },
73
+ });
@@ -0,0 +1,10 @@
1
+ import type { ShellSourceCode } from '../../language/shell-source-code.js';
2
+ import type { ShellNode } from '../../language/syntax-view.js';
3
+ /**
4
+ * The `[start, end)` UTF-16 range of `operator` at `node`'s `OpPos`, or
5
+ * `undefined` when the record has no valid `OpPos` or the text there is not
6
+ * `operator`.
7
+ *
8
+ * @internal
9
+ */
10
+ export declare function operatorRange(sourceCode: ShellSourceCode, node: ShellNode, operator: string): [number, number] | undefined;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The `[start, end)` UTF-16 range of `operator` at `node`'s `OpPos`, or
3
+ * `undefined` when the record has no valid `OpPos` or the text there is not
4
+ * `operator`.
5
+ *
6
+ * @internal
7
+ */
8
+ export function operatorRange(sourceCode, node, operator) {
9
+ const syntax = sourceCode.getSyntax(node);
10
+ const pos = syntax?.OpPos;
11
+ if (pos?.IsValid !== true)
12
+ return undefined;
13
+ const start = sourceCode.getIndexFromSyntaxPos(pos);
14
+ return sourceCode.text.startsWith(operator, start) ? [start, start + operator.length] : undefined;
15
+ }
@@ -0,0 +1,12 @@
1
+ import type { ShellSourceCode } from '../../language/shell-source-code.js';
2
+ import type { ShellNode, ShellNodeOf } from '../../language/syntax-view.js';
3
+ /**
4
+ * The word that names `command` in `node`, when `command` is what `node`
5
+ * effectively runs and a shell without it can reach `node` (see the module
6
+ * comment), or `undefined` otherwise. Reporting this word rather than the whole
7
+ * command matters for `[[`: the parser ends that command at the first `&&` or
8
+ * `||`, so the node would cover an arbitrary part of the expression.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare function reachableCommandWord(sourceCode: ShellSourceCode, node: ShellNodeOf<'CallExpr'>, command: string): ShellNode | undefined;