@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,69 @@
1
+ import { createRule } from './create-rule.js';
2
+ import { shebangShell } from './shebang-interpreter.js';
3
+ /**
4
+ * Interpreter basenames that are POSIX shells for this rule's purpose: they
5
+ * implement the POSIX shell language and none of the bash extensions.
6
+ */
7
+ const POSIX_SHELLS = new Set(['sh', 'dash', 'ash']);
8
+ /** The UTF-8 byte order mark as it appears in a decoded string. */
9
+ const BOM = '\uFEFF';
10
+ /**
11
+ * Which mismatch, if any, the shebang's shell and the dialect form. `undefined`
12
+ * means the pair is consistent or outside this rule's bash/POSIX contract.
13
+ */
14
+ function mismatchFor(shell, dialect) {
15
+ if (shell === undefined) {
16
+ return undefined;
17
+ }
18
+ if (dialect === 'bash' && POSIX_SHELLS.has(shell)) {
19
+ return 'posixShebangBashDialect';
20
+ }
21
+ if (dialect === 'posix' && shell === 'bash') {
22
+ return 'bashShebangPosixDialect';
23
+ }
24
+ return undefined;
25
+ }
26
+ /**
27
+ * The rule module registered as `shebang-matches-dialect`.
28
+ *
29
+ * @internal
30
+ */
31
+ export const shebangMatchesDialect = createRule({
32
+ name: 'shebang-matches-dialect',
33
+ meta: {
34
+ type: 'problem',
35
+ docs: {
36
+ description: 'Require the shebang to name the same kind of shell as languageOptions.dialect',
37
+ },
38
+ schema: [],
39
+ messages: {
40
+ posixShebangBashDialect: "The shebang '{{shebang}}' names a POSIX shell, but this file is linted as bash, so bash-only advice (such as pipefail) can break it. Set the dialect for these files with `languageOptions: { dialect: 'posix' }` in your eslint.config.js, or change the shebang to a bash one such as '#!/usr/bin/env bash'.",
41
+ bashShebangPosixDialect: "The shebang '{{shebang}}' names bash, but this file is linted as posix, so bash syntax goes unchecked. Set the dialect for these files with `languageOptions: { dialect: 'bash' }` in your eslint.config.js, or change the shebang to a POSIX one such as '#!/bin/sh'.",
42
+ },
43
+ },
44
+ create(context) {
45
+ const { sourceCode } = context;
46
+ return {
47
+ File() {
48
+ const firstLine = sourceCode.lines[0] ?? '';
49
+ const shebangLine = firstLine.startsWith(BOM) ? firstLine.slice(1) : firstLine;
50
+ if (!shebangLine.startsWith('#!')) {
51
+ return;
52
+ }
53
+ const messageId = mismatchFor(shebangShell(shebangLine.slice('#!'.length)), sourceCode.dialect);
54
+ if (messageId === undefined) {
55
+ return;
56
+ }
57
+ // Line 1 in full, from the line's own length.
58
+ context.report({
59
+ loc: {
60
+ start: { line: 1, column: 1 },
61
+ end: { line: 1, column: firstLine.length + 1 },
62
+ },
63
+ messageId,
64
+ data: { shebang: shebangLine.trimEnd() },
65
+ });
66
+ },
67
+ };
68
+ },
69
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Wrapper data that built-in rules add to `sh-ast/analyze`'s
3
+ * `DEFAULT_TRANSPARENT_WRAPPERS` when they ask
4
+ * `ShellSourceCode.resolveInvokedCommand` which command an invocation runs.
5
+ *
6
+ * Everything here is a `WrapperSpec`: data that tells `resolveArgv0` how to
7
+ * find the word a wrapper runs. No rule logic lives here, and no entry makes
8
+ * a safety judgment. Each rule decides which of these entries it follows.
9
+ */
10
+ import type { WrapperSpec } from 'sh-ast/analyze';
11
+ /**
12
+ * Bash's `builtin [shell-builtin [args …]]`: runs the named builtin, bypassing
13
+ * any function of the same name. It takes no options of its own, so the word
14
+ * after it is the command it runs.
15
+ *
16
+ * `sh-ast/analyze`'s default wrapper table leaves `builtin` out, so
17
+ * `builtin "$x"` would otherwise resolve to the static command `builtin`.
18
+ *
19
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Bash-Builtins — `builtin`
20
+ * @internal
21
+ */
22
+ export declare const BUILTIN_WRAPPER: WrapperSpec;
23
+ /**
24
+ * A shell invoked as a command (`bash …`, `sh …`), modeled as a wrapper so the
25
+ * command it runs is found the same way as for `env` or `sudo`:
26
+ *
27
+ * - `bash script.sh` runs `script.sh`; the options before it are skipped.
28
+ * - `bash -c STRING` (and `-c` clustered after one other option letter, such
29
+ * as `bash -lc STRING` or `bash -ec STRING`) runs code embedded in a
30
+ * string. `-c` is an unresolvable flag, so `resolveArgv0` reports the
31
+ * effective command as `{ static: false, reason: 'embedded-command' }`
32
+ * instead of guessing at the string's contents.
33
+ * - `bash -s` (and `bash` with no operand) reads its commands from standard
34
+ * input. `-s` stops the chain: the shell itself is the effective command.
35
+ *
36
+ * `+`-prefixed option letters (`+x`) turn an option off and take no operand.
37
+ * `-o`/`+o` and Bash's `-O`/`+O` take an option name. An option this entry
38
+ * does not know makes `resolveArgv0` report `'unknown-flag'`, never a guess.
39
+ *
40
+ * `resolveArgv0` clusters `noArgFlags` with a following `argFlags` entry, but
41
+ * not with `unresolvableFlags` or `stopsChainFlags`, so the one-letter
42
+ * clusters ending in `c` or `s` are listed as exact tokens. Longer clusters
43
+ * (`-xec`) end in `'unknown-flag'`.
44
+ *
45
+ * Names match exactly, like every `sh-ast/analyze` wrapper: `/bin/bash` is not
46
+ * `bash`.
47
+ *
48
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Invoking-Bash
49
+ * @see https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html — POSIX `sh`
50
+ * @internal
51
+ */
52
+ export declare const SHELL_WRAPPER: WrapperSpec;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Bash's `builtin [shell-builtin [args …]]`: runs the named builtin, bypassing
3
+ * any function of the same name. It takes no options of its own, so the word
4
+ * after it is the command it runs.
5
+ *
6
+ * `sh-ast/analyze`'s default wrapper table leaves `builtin` out, so
7
+ * `builtin "$x"` would otherwise resolve to the static command `builtin`.
8
+ *
9
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Bash-Builtins — `builtin`
10
+ * @internal
11
+ */
12
+ export const BUILTIN_WRAPPER = { names: ['builtin'] };
13
+ /**
14
+ * The single-letter shell options that take no operand when a shell is
15
+ * invoked: every option letter the `set` builtin accepts, plus Bash's
16
+ * invocation-only `-i`, `-l`, `-r` and `-D`. `-c`, `-s` and `-o`/`-O` are
17
+ * absent on purpose: they are modeled separately in {@link SHELL_WRAPPER}.
18
+ *
19
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Invoking-Bash
20
+ * @see https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin
21
+ * @see https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html — POSIX `sh` options
22
+ */
23
+ const SHELL_NO_ARG_LETTERS = [
24
+ 'a',
25
+ 'b',
26
+ 'e',
27
+ 'f',
28
+ 'h',
29
+ 'k',
30
+ 'm',
31
+ 'n',
32
+ 'p',
33
+ 't',
34
+ 'u',
35
+ 'v',
36
+ 'x',
37
+ 'B',
38
+ 'C',
39
+ 'E',
40
+ 'H',
41
+ 'P',
42
+ 'T',
43
+ 'i',
44
+ 'l',
45
+ 'r',
46
+ 'D',
47
+ ];
48
+ /**
49
+ * A shell invoked as a command (`bash …`, `sh …`), modeled as a wrapper so the
50
+ * command it runs is found the same way as for `env` or `sudo`:
51
+ *
52
+ * - `bash script.sh` runs `script.sh`; the options before it are skipped.
53
+ * - `bash -c STRING` (and `-c` clustered after one other option letter, such
54
+ * as `bash -lc STRING` or `bash -ec STRING`) runs code embedded in a
55
+ * string. `-c` is an unresolvable flag, so `resolveArgv0` reports the
56
+ * effective command as `{ static: false, reason: 'embedded-command' }`
57
+ * instead of guessing at the string's contents.
58
+ * - `bash -s` (and `bash` with no operand) reads its commands from standard
59
+ * input. `-s` stops the chain: the shell itself is the effective command.
60
+ *
61
+ * `+`-prefixed option letters (`+x`) turn an option off and take no operand.
62
+ * `-o`/`+o` and Bash's `-O`/`+O` take an option name. An option this entry
63
+ * does not know makes `resolveArgv0` report `'unknown-flag'`, never a guess.
64
+ *
65
+ * `resolveArgv0` clusters `noArgFlags` with a following `argFlags` entry, but
66
+ * not with `unresolvableFlags` or `stopsChainFlags`, so the one-letter
67
+ * clusters ending in `c` or `s` are listed as exact tokens. Longer clusters
68
+ * (`-xec`) end in `'unknown-flag'`.
69
+ *
70
+ * Names match exactly, like every `sh-ast/analyze` wrapper: `/bin/bash` is not
71
+ * `bash`.
72
+ *
73
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Invoking-Bash
74
+ * @see https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html — POSIX `sh`
75
+ * @internal
76
+ */
77
+ export const SHELL_WRAPPER = {
78
+ names: ['bash', 'sh', 'dash', 'ksh', 'mksh', 'zsh'],
79
+ noArgFlags: [
80
+ ...SHELL_NO_ARG_LETTERS.map((letter) => `-${letter}`),
81
+ '--debugger',
82
+ '--dump-po-strings',
83
+ '--dump-strings',
84
+ '--help',
85
+ '--login',
86
+ '--noediting',
87
+ '--noprofile',
88
+ '--norc',
89
+ '--posix',
90
+ '--pretty-print',
91
+ '--restricted',
92
+ '--verbose',
93
+ '--version',
94
+ ],
95
+ noArgFlagPattern: /^\+[abefhkmnptuvxBCEHPT]+$/,
96
+ argFlags: ['-o', '+o', '-O', '+O', '--init-file', '--rcfile'],
97
+ unresolvableFlags: ['-c', ...SHELL_NO_ARG_LETTERS.map((letter) => `-${letter}c`)],
98
+ stopsChainFlags: ['-s', ...SHELL_NO_ARG_LETTERS.map((letter) => `-${letter}s`)],
99
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Compiling regular-expression sources that a rule takes from its options and
3
+ * tests against text from the linted script.
4
+ *
5
+ * The configuration is trusted, but the subject is not: when the plugin lints
6
+ * a repository it does not control, the script's author picks the names a
7
+ * pattern runs against. V8's backtracking engine takes time exponential in the
8
+ * subject's length for a pattern that repeats a group which itself repeats,
9
+ * such as `^([A-Z]+_?)*_KEY$` against `AAAA…Ax`. {@link compileUserPattern}
10
+ * rejects the common form of such a pattern as a configuration error, so the
11
+ * accident of writing one is caught. It is not a proof that an accepted
12
+ * pattern is safe.
13
+ *
14
+ * The check is a static star-height test: a quantifier that repeats (`*`, `+`,
15
+ * `{n,}` or `{n,m}` with `m > 1`) applied to a group that already contains a
16
+ * repeating quantifier is rejected. It is conservative. A nested repetition
17
+ * that happens to be linear, such as `(a*b)*`, is rejected too; a rewrite
18
+ * without the nesting exists only for some such patterns. It does not detect every
19
+ * super-linear pattern: an alternation whose branches overlap under a
20
+ * repetition (`^([A-Z]|[A-Z0-9])+_KEY$`, `(a|a)+$`) passes, and so do
21
+ * adjacent quantifiers that can match the same text (`.*.*.*x`), whose cost is
22
+ * polynomial in the name's length. The rule docs list these limits.
23
+ *
24
+ * @see https://github.com/mike-north/eslint-sh/issues/221
25
+ */
26
+ /**
27
+ * Where in a source a rejected nested repetition was found, for the error message.
28
+ *
29
+ * @internal
30
+ */
31
+ export interface NestedRepetition {
32
+ /** The outer repeating quantifier as written, such as `*` or `{2,}`. */
33
+ readonly outer: string;
34
+ /** The text of the group the outer quantifier repeats, such as `([A-Z]+_?)`. */
35
+ readonly group: string;
36
+ }
37
+ /**
38
+ * Finds a repeating quantifier applied to a group that already contains one,
39
+ * the shape that makes a backtracking engine exponential. Returns the first
40
+ * such nesting, or `undefined` when the source has star height of at most one.
41
+ *
42
+ * The source is assumed to be a valid regular expression; the caller compiles
43
+ * it first.
44
+ *
45
+ * @internal
46
+ */
47
+ export declare function findNestedRepetition(source: string): NestedRepetition | undefined;
48
+ /**
49
+ * Compiles a regular-expression source from a rule option that will be tested
50
+ * against script-controlled text. Throws a `TypeError` naming the rule and the
51
+ * option when the source is not a valid expression, or when it nests one
52
+ * repetition inside another (see {@link findNestedRepetition}). ESLint reports
53
+ * the error from the rule's `create()`, so it surfaces on the first file linted
54
+ * with the rule ("Occurred while linting <file>"), not when the config loads.
55
+ *
56
+ * @param rule - The rule's full name, such as `sh/prefer-local`.
57
+ * @param option - The option as the user wrote it, such as `patterns[0]`.
58
+ * @param source - The regular-expression source.
59
+ * @param flags - The flags to compile with.
60
+ *
61
+ * @internal
62
+ */
63
+ export declare function compileUserPattern(rule: string, option: string, source: string, flags: string): RegExp;
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Compiling regular-expression sources that a rule takes from its options and
3
+ * tests against text from the linted script.
4
+ *
5
+ * The configuration is trusted, but the subject is not: when the plugin lints
6
+ * a repository it does not control, the script's author picks the names a
7
+ * pattern runs against. V8's backtracking engine takes time exponential in the
8
+ * subject's length for a pattern that repeats a group which itself repeats,
9
+ * such as `^([A-Z]+_?)*_KEY$` against `AAAA…Ax`. {@link compileUserPattern}
10
+ * rejects the common form of such a pattern as a configuration error, so the
11
+ * accident of writing one is caught. It is not a proof that an accepted
12
+ * pattern is safe.
13
+ *
14
+ * The check is a static star-height test: a quantifier that repeats (`*`, `+`,
15
+ * `{n,}` or `{n,m}` with `m > 1`) applied to a group that already contains a
16
+ * repeating quantifier is rejected. It is conservative. A nested repetition
17
+ * that happens to be linear, such as `(a*b)*`, is rejected too; a rewrite
18
+ * without the nesting exists only for some such patterns. It does not detect every
19
+ * super-linear pattern: an alternation whose branches overlap under a
20
+ * repetition (`^([A-Z]|[A-Z0-9])+_KEY$`, `(a|a)+$`) passes, and so do
21
+ * adjacent quantifiers that can match the same text (`.*.*.*x`), whose cost is
22
+ * polynomial in the name's length. The rule docs list these limits.
23
+ *
24
+ * @see https://github.com/mike-north/eslint-sh/issues/221
25
+ */
26
+ /** Matches a `{n}`, `{n,}` or `{n,m}` quantifier at the start of the string. */
27
+ const BRACE_QUANTIFIER = /^\{\d+(?:,\d*)?\}/u;
28
+ /**
29
+ * Reads the quantifier starting at `source[i]`, if there is one. Returns its
30
+ * length in characters, including a trailing lazy `?`, and whether it can
31
+ * match its atom more than once.
32
+ */
33
+ function readQuantifier(source, i) {
34
+ const char = source[i];
35
+ let length;
36
+ let repeats;
37
+ if (char === '*' || char === '+') {
38
+ length = 1;
39
+ repeats = true;
40
+ }
41
+ else if (char === '?') {
42
+ length = 1;
43
+ repeats = false;
44
+ }
45
+ else if (char === '{') {
46
+ const match = BRACE_QUANTIFIER.exec(source.slice(i));
47
+ // Without the `u` flag a `{` that does not form a quantifier is a literal.
48
+ if (match === null)
49
+ return undefined;
50
+ length = match[0].length;
51
+ // `{n}` repeats at most n times, `{n,}` without bound, `{n,m}` at most m times.
52
+ const bounds = match[0].slice(1, -1);
53
+ const comma = bounds.indexOf(',');
54
+ const upper = comma === -1
55
+ ? Number(bounds)
56
+ : comma === bounds.length - 1
57
+ ? Infinity
58
+ : Number(bounds.slice(comma + 1));
59
+ repeats = upper > 1;
60
+ }
61
+ else {
62
+ return undefined;
63
+ }
64
+ // A lazy quantifier (`*?`, `{2,}?`) backtracks the same way.
65
+ if (source[i + length] === '?')
66
+ length++;
67
+ return { length, repeats };
68
+ }
69
+ /**
70
+ * Returns the index just past the escape sequence starting at `source[i]`
71
+ * (which is `\`). Braced and angled forms (`\u{…}`, `\p{…}`, `\k<…>`) are
72
+ * consumed whole, so their braces are not read as a quantifier.
73
+ */
74
+ function skipEscape(source, i) {
75
+ const next = source[i + 1];
76
+ const close = next === 'k' ? '>' : '}';
77
+ const open = next === 'k' ? '<' : '{';
78
+ if ((next === 'u' || next === 'p' || next === 'P' || next === 'k') && source[i + 2] === open) {
79
+ const end = source.indexOf(close, i + 3);
80
+ if (end !== -1)
81
+ return end + 1;
82
+ }
83
+ return i + 2;
84
+ }
85
+ /** Returns the index just past the character class starting at `source[i]` (which is `[`). */
86
+ function skipClass(source, i) {
87
+ let j = i + 1;
88
+ if (source[j] === '^')
89
+ j++;
90
+ while (j < source.length && source[j] !== ']') {
91
+ j = source[j] === '\\' ? j + 2 : j + 1;
92
+ }
93
+ return j + 1;
94
+ }
95
+ /**
96
+ * Finds a repeating quantifier applied to a group that already contains one,
97
+ * the shape that makes a backtracking engine exponential. Returns the first
98
+ * such nesting, or `undefined` when the source has star height of at most one.
99
+ *
100
+ * The source is assumed to be a valid regular expression; the caller compiles
101
+ * it first.
102
+ *
103
+ * @internal
104
+ */
105
+ export function findNestedRepetition(source) {
106
+ const stack = [{ start: 0, containsRepetition: false }];
107
+ let i = 0;
108
+ while (i < source.length) {
109
+ const char = source[i];
110
+ // The atom just read: where it starts, where it ends, and whether it
111
+ // already contains a repetition (only a group can).
112
+ let atomStart = i;
113
+ let atomEnd;
114
+ let atomRepeats = false;
115
+ if (char === '\\') {
116
+ atomEnd = skipEscape(source, i);
117
+ }
118
+ else if (char === '[') {
119
+ atomEnd = skipClass(source, i);
120
+ }
121
+ else if (char === '(') {
122
+ stack.push({ start: i, containsRepetition: false });
123
+ i++;
124
+ continue;
125
+ }
126
+ else if (char === ')') {
127
+ const frame = stack.pop();
128
+ // An unbalanced `)` is a syntax error the caller already rejected.
129
+ if (frame === undefined || stack.length === 0)
130
+ return undefined;
131
+ atomStart = frame.start;
132
+ atomEnd = i + 1;
133
+ atomRepeats = frame.containsRepetition;
134
+ }
135
+ else {
136
+ atomEnd = i + 1;
137
+ }
138
+ const quantifier = readQuantifier(source, atomEnd);
139
+ if (quantifier?.repeats === true && atomRepeats) {
140
+ return {
141
+ outer: source.slice(atomEnd, atomEnd + quantifier.length),
142
+ group: source.slice(atomStart, atomEnd),
143
+ };
144
+ }
145
+ const top = stack.at(-1);
146
+ if (top !== undefined && (atomRepeats || quantifier?.repeats === true)) {
147
+ top.containsRepetition = true;
148
+ }
149
+ i = atomEnd + (quantifier?.length ?? 0);
150
+ }
151
+ return undefined;
152
+ }
153
+ /**
154
+ * Compiles a regular-expression source from a rule option that will be tested
155
+ * against script-controlled text. Throws a `TypeError` naming the rule and the
156
+ * option when the source is not a valid expression, or when it nests one
157
+ * repetition inside another (see {@link findNestedRepetition}). ESLint reports
158
+ * the error from the rule's `create()`, so it surfaces on the first file linted
159
+ * with the rule ("Occurred while linting <file>"), not when the config loads.
160
+ *
161
+ * @param rule - The rule's full name, such as `sh/prefer-local`.
162
+ * @param option - The option as the user wrote it, such as `patterns[0]`.
163
+ * @param source - The regular-expression source.
164
+ * @param flags - The flags to compile with.
165
+ *
166
+ * @internal
167
+ */
168
+ export function compileUserPattern(rule, option, source, flags) {
169
+ let pattern;
170
+ try {
171
+ pattern = new RegExp(source, flags);
172
+ }
173
+ catch {
174
+ throw new TypeError(`${rule}: \`${option}\` must be a valid regular expression; got ${JSON.stringify(source)}.`);
175
+ }
176
+ const nested = findNestedRepetition(source);
177
+ if (nested !== undefined) {
178
+ throw new TypeError(`${rule}: \`${option}\` (${JSON.stringify(source)}) repeats ${nested.group} with \`${nested.outer}\`, ` +
179
+ 'and that group already repeats. The pattern runs against names the linted script chooses, ' +
180
+ 'and a nested repetition can take exponential time on a long name. Rewrite it with one level ' +
181
+ 'of repetition, for example `[A-Z0-9_]*` instead of `([A-Z]+_?)*`.');
182
+ }
183
+ return pattern;
184
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The faithful shell analysis surface (`@eslint-sh/eslint-plugin/syntax/analyze`): the complete
3
+ * public surface of `sh-ast/analyze`, re-exported unchanged from the very `sh-ast` instance this
4
+ * plugin's language parses with.
5
+ *
6
+ * It provides `enumerateCommands`, `resolveArgv0` (whose `chainIndices` locate a resolved command's
7
+ * arguments in its site's `argv`), `DEFAULT_TRANSPARENT_WRAPPERS`, `WrapperSpec`, the raw
8
+ * `resolveWord(Node)`, and the named operator constants with `rawOperator` for comparing and
9
+ * constructing the operator values those records carry. All of it works on the faithful records
10
+ * from `@eslint-sh/eslint-plugin/syntax`. The plugin root's `resolveWord` takes a lint-view
11
+ * `ShellNode` instead; the two share a name but not an input contract, so this subpath is kept
12
+ * separate from the root.
13
+ *
14
+ * Like `@eslint-sh/eslint-plugin/syntax`, it tracks the plugin's pinned `sh-ast` minor and never
15
+ * wraps or renames anything.
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ export * from 'sh-ast/analyze';
@@ -0,0 +1,21 @@
1
+ /* eslint-disable @api-extractor-tools/package-documentation -- this file is the barrel of the
2
+ `./syntax/analyze` entry point (package.json `exports`); the rule only recognizes the root barrel. */
3
+ /**
4
+ * The faithful shell analysis surface (`@eslint-sh/eslint-plugin/syntax/analyze`): the complete
5
+ * public surface of `sh-ast/analyze`, re-exported unchanged from the very `sh-ast` instance this
6
+ * plugin's language parses with.
7
+ *
8
+ * It provides `enumerateCommands`, `resolveArgv0` (whose `chainIndices` locate a resolved command's
9
+ * arguments in its site's `argv`), `DEFAULT_TRANSPARENT_WRAPPERS`, `WrapperSpec`, the raw
10
+ * `resolveWord(Node)`, and the named operator constants with `rawOperator` for comparing and
11
+ * constructing the operator values those records carry. All of it works on the faithful records
12
+ * from `@eslint-sh/eslint-plugin/syntax`. The plugin root's `resolveWord` takes a lint-view
13
+ * `ShellNode` instead; the two share a name but not an input contract, so this subpath is kept
14
+ * separate from the root.
15
+ *
16
+ * Like `@eslint-sh/eslint-plugin/syntax`, it tracks the plugin's pinned `sh-ast` minor and never
17
+ * wraps or renames anything.
18
+ *
19
+ * @packageDocumentation
20
+ */
21
+ export * from 'sh-ast/analyze';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The faithful shell syntax surface (`@eslint-sh/eslint-plugin/syntax`): the complete public
3
+ * surface of `sh-ast`'s root entry point, re-exported unchanged from the very `sh-ast` instance
4
+ * this plugin's language parses with.
5
+ *
6
+ * Use it when a custom rule edits or analyzes the parser's own records rather than the plugin's
7
+ * lint view: `walk`, `parseSync`, `printSync` and `PrintOptions`, the record types (`Node`,
8
+ * `Word`, `CallExpr`, `SyntaxPos`, `GoString`, …), and the operator and language constants.
9
+ * Importing them from here, instead of installing `sh-ast` separately, means a rule always gets
10
+ * the same `sh-ast` version and module instance as the language that produced its syntax. Under
11
+ * 0.x a different `sh-ast` minor is a breaking change, and records from one copy are not
12
+ * guaranteed to be accepted by another.
13
+ *
14
+ * This entry point tracks the plugin's pinned `sh-ast` minor, so an `sh-ast` 0.x minor bump is a
15
+ * minor release of this plugin. It never wraps or renames anything: every name has `sh-ast`'s
16
+ * own contract. The plugin root (`@eslint-sh/eslint-plugin`) keeps the view-aware helpers such
17
+ * as `resolveWord(ShellNode)`, which differ from the raw helpers of the same name in
18
+ * `@eslint-sh/eslint-plugin/syntax/analyze`. That is why these names live on their own subpath
19
+ * and are not merged into the root.
20
+ *
21
+ * @packageDocumentation
22
+ */
23
+ export * from 'sh-ast';
@@ -0,0 +1,25 @@
1
+ /* eslint-disable @api-extractor-tools/package-documentation -- this file is the barrel of the
2
+ `./syntax` entry point (package.json `exports`); the rule only recognizes the root barrel. */
3
+ /**
4
+ * The faithful shell syntax surface (`@eslint-sh/eslint-plugin/syntax`): the complete public
5
+ * surface of `sh-ast`'s root entry point, re-exported unchanged from the very `sh-ast` instance
6
+ * this plugin's language parses with.
7
+ *
8
+ * Use it when a custom rule edits or analyzes the parser's own records rather than the plugin's
9
+ * lint view: `walk`, `parseSync`, `printSync` and `PrintOptions`, the record types (`Node`,
10
+ * `Word`, `CallExpr`, `SyntaxPos`, `GoString`, …), and the operator and language constants.
11
+ * Importing them from here, instead of installing `sh-ast` separately, means a rule always gets
12
+ * the same `sh-ast` version and module instance as the language that produced its syntax. Under
13
+ * 0.x a different `sh-ast` minor is a breaking change, and records from one copy are not
14
+ * guaranteed to be accepted by another.
15
+ *
16
+ * This entry point tracks the plugin's pinned `sh-ast` minor, so an `sh-ast` 0.x minor bump is a
17
+ * minor release of this plugin. It never wraps or renames anything: every name has `sh-ast`'s
18
+ * own contract. The plugin root (`@eslint-sh/eslint-plugin`) keeps the view-aware helpers such
19
+ * as `resolveWord(ShellNode)`, which differ from the raw helpers of the same name in
20
+ * `@eslint-sh/eslint-plugin/syntax/analyze`. That is why these names live on their own subpath
21
+ * and are not merged into the root.
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+ export * from 'sh-ast';
package/dist/version.d.ts CHANGED
@@ -11,4 +11,3 @@
11
11
  * @internal
12
12
  */
13
13
  export declare function readPackageVersion(): string;
14
- //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -26,4 +26,3 @@ export function readPackageVersion() {
26
26
  }
27
27
  throw new Error(`@eslint-sh/eslint-plugin: package.json at ${packageJsonPath} is missing a string "version" field`);
28
28
  }
29
- //# sourceMappingURL=version.js.map