@eslint-sh/eslint-plugin 0.4.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,109 @@
1
+ /**
2
+ * The key a convention is bound under, both in a consuming rule's options
3
+ * object and in the plugin's `settings.sh` namespace.
4
+ */
5
+ const DECLARATION_CONVENTION_KEY = 'declarationConvention';
6
+ /** `value` is an object (not `null`) whose properties can be read. */
7
+ function isObjectLike(value) {
8
+ return (typeof value === 'object' || typeof value === 'function') && value !== null;
9
+ }
10
+ /** A short description of a value that is not a convention, for an error message. */
11
+ function describeValue(value) {
12
+ if (value === null)
13
+ return 'null';
14
+ if (Array.isArray(value))
15
+ return 'an array';
16
+ if (isObjectLike(value)) {
17
+ return typeof value.declaredCommands === 'undefined'
18
+ ? 'an object with no `declaredCommands` method'
19
+ : `an object whose \`declaredCommands\` is ${typeof value.declaredCommands}, not a function`;
20
+ }
21
+ switch (typeof value) {
22
+ case 'string':
23
+ return `string ${JSON.stringify(value)}`;
24
+ case 'number':
25
+ case 'boolean':
26
+ case 'bigint':
27
+ case 'symbol':
28
+ return `${typeof value} ${String(value)}`;
29
+ default:
30
+ return typeof value;
31
+ }
32
+ }
33
+ /**
34
+ * Checks that `value`, configured at `setting`, implements
35
+ * {@link DeclarationConvention}: a `declaredCommands` function, and, when a
36
+ * `removeDeclaration` or `declarationHint` hook is present at all, a function
37
+ * there too. Throws an
38
+ * error naming `setting` otherwise.
39
+ */
40
+ function assertDeclarationConvention(value, setting) {
41
+ const expected = 'expected a declaration convention: an object with a `declaredCommands(sourceCode)` method';
42
+ if (!isObjectLike(value) || typeof value.declaredCommands !== 'function') {
43
+ throw new Error(`Invalid ${setting}: ${expected}, but got ${describeValue(value)}.`);
44
+ }
45
+ for (const hook of ['removeDeclaration', 'declarationHint']) {
46
+ if (value[hook] !== undefined && typeof value[hook] !== 'function') {
47
+ throw new Error(`Invalid ${setting}: its optional \`${hook}\` hook must be a function when present, but got ${typeof value[hook]}.`);
48
+ }
49
+ }
50
+ }
51
+ /**
52
+ * Resolves the declaration convention bound for the rule running in
53
+ * `context`: the rule's `declarationConvention` option, else
54
+ * `settings.sh.declarationConvention`, else `null` (the rule is inert).
55
+ *
56
+ * Throws an `Error` naming the offending setting when the configured value is
57
+ * present but does not implement `declaredCommands` (or has a non-function
58
+ * `removeDeclaration`). The message names where the value came from and what
59
+ * it was, for example one beginning
60
+ * `Invalid settings.sh.declarationConvention: expected a declaration convention`
61
+ * and ending `but got string "jq".` Call it from `create()`, so the error
62
+ * surfaces as ESLint's
63
+ * configuration message for the file being linted.
64
+ *
65
+ * A rule that receives `null` should register no listeners: with no
66
+ * convention bound, a convention consumer has nothing to check.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * import {
71
+ * defineShellRule,
72
+ * readDeclaredCommands,
73
+ * resolveDeclarationConvention,
74
+ * } from '@eslint-sh/eslint-plugin';
75
+ *
76
+ * export const maxDeclaredCommands = defineShellRule({
77
+ * meta: { schema: [], messages: { tooMany: 'Declare at most 5 commands.' } },
78
+ * create(context) {
79
+ * const convention = resolveDeclarationConvention(context);
80
+ * if (convention === null) return {};
81
+ * return {
82
+ * File(node) {
83
+ * // Validated declarations for the file `context` is linting.
84
+ * if (readDeclaredCommands(convention, context).length > 5) {
85
+ * context.report({ node, messageId: 'tooMany' });
86
+ * }
87
+ * },
88
+ * };
89
+ * },
90
+ * });
91
+ * ```
92
+ *
93
+ * @public
94
+ */
95
+ export function resolveDeclarationConvention(context) {
96
+ const ruleOptions = context.options[0];
97
+ if (isObjectLike(ruleOptions) && ruleOptions[DECLARATION_CONVENTION_KEY] !== undefined) {
98
+ const fromOption = ruleOptions[DECLARATION_CONVENTION_KEY];
99
+ assertDeclarationConvention(fromOption, `option \`${DECLARATION_CONVENTION_KEY}\` of rule "${context.id}"`);
100
+ return fromOption;
101
+ }
102
+ const shSettings = context.settings.sh;
103
+ if (isObjectLike(shSettings) && shSettings[DECLARATION_CONVENTION_KEY] !== undefined) {
104
+ const fromSettings = shSettings[DECLARATION_CONVENTION_KEY];
105
+ assertDeclarationConvention(fromSettings, `settings.sh.${DECLARATION_CONVENTION_KEY}`);
106
+ return fromSettings;
107
+ }
108
+ return null;
109
+ }
@@ -0,0 +1,19 @@
1
+ import type { WrapperSpec } from 'sh-ast/analyze';
2
+ /**
3
+ * Reads `settings.sh.networkWrapper`: the configured name or `WrapperSpec`,
4
+ * or `undefined` when it is not set. Throws an error that starts
5
+ * `Invalid settings.sh.networkWrapper:` when the value is malformed. Call it
6
+ * from `create()`, so the error surfaces as ESLint's configuration message.
7
+ *
8
+ * @internal
9
+ */
10
+ export declare function readNetworkWrapperSetting(settings: Readonly<Record<string, unknown>>): string | WrapperSpec | undefined;
11
+ /**
12
+ * The `WrapperSpec` for the network wrapper set in `settings.sh.networkWrapper`,
13
+ * for a rule that looks through wrappers, or `null` when none is set. A name
14
+ * that is one of `sh-ast/analyze`'s default wrappers (`timeout`) keeps that
15
+ * entry's known options; any other name is a wrapper with no options.
16
+ *
17
+ * @internal
18
+ */
19
+ export declare function networkWrapperSpec(settings: Readonly<Record<string, unknown>>): WrapperSpec | null;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The shared `settings.sh.networkWrapper` binding: the one place a project
3
+ * names the wrapper its network commands must run through, so every rule
4
+ * whose findings depend on that wrapper reads the same declaration.
5
+ *
6
+ * Two different statements are involved, and this setting makes only one of
7
+ * them:
8
+ *
9
+ * - **Policy:** network commands must run through this wrapper. That is what
10
+ * `sh/require-network-wrapper` enforces, and this setting states it.
11
+ * - **Fact:** this wrapper runs the command named after it. The policy implies
12
+ * the fact (a wrapper that encloses `curl` runs `curl`), so
13
+ * `sh/no-undeclared-commands` and `sh/no-unused-declarations` look through
14
+ * the network wrapper when it is set here. The converse does not hold: a
15
+ * transparent wrapper such as `with-lock` is not the network wrapper, and
16
+ * stays in those rules' `transparentWrappers` option.
17
+ *
18
+ * ESLint gives a rule no way to read another rule's options, so a wrapper set
19
+ * only as `require-network-wrapper`'s `wrapper` option is invisible to the
20
+ * declaration rules. Shared settings are visible to every rule; that is why
21
+ * the one declaration lives here.
22
+ */
23
+ import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
24
+ /** The setting's dotted path, as error messages name it. */
25
+ const SETTING = 'settings.sh.networkWrapper';
26
+ /**
27
+ * The `WrapperSpec` fields the setting accepts: the JSON-expressible ones,
28
+ * the same set `sh/require-network-wrapper`'s `wrapper` option schema allows.
29
+ */
30
+ const STRING_LIST_FIELDS = ['noArgFlags', 'argFlags', 'unresolvableFlags', 'stopsChainFlags'];
31
+ const ASSIGNMENT_OPERANDS = ['shell-identifier', 'env', 'sudo'];
32
+ const KNOWN_FIELDS = new Set([
33
+ 'names',
34
+ ...STRING_LIST_FIELDS,
35
+ 'positionalOperandsBeforeCommand',
36
+ 'assignmentOperands',
37
+ ]);
38
+ /** `true` when `value` is an array of non-empty strings. */
39
+ function isStringList(value) {
40
+ return (Array.isArray(value) &&
41
+ value.every((item) => typeof item === 'string' && item.length > 0));
42
+ }
43
+ /** Throws the setting's configuration error with `detail`. */
44
+ function invalid(detail) {
45
+ throw new Error(`Invalid ${SETTING}: ${detail}`);
46
+ }
47
+ /**
48
+ * Checks a configured value and returns it typed: a non-empty command name,
49
+ * or a `WrapperSpec` object made of the JSON-expressible fields.
50
+ */
51
+ function validate(value) {
52
+ if (typeof value === 'string') {
53
+ if (value.length === 0)
54
+ invalid('expected a command name or a wrapper spec, but got "".');
55
+ return value;
56
+ }
57
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
58
+ const got = value === null ? 'null' : Array.isArray(value) ? 'an array' : typeof value;
59
+ invalid(`expected a command name or a wrapper spec ({ names: [...] }), but got ${got}.`);
60
+ }
61
+ const spec = value;
62
+ for (const key of Object.keys(spec)) {
63
+ if (!KNOWN_FIELDS.has(key))
64
+ invalid(`unknown wrapper spec field \`${key}\`.`);
65
+ }
66
+ if (!isStringList(spec.names) || spec.names.length === 0) {
67
+ invalid('`names` must be a non-empty array of non-empty strings.');
68
+ }
69
+ for (const field of STRING_LIST_FIELDS) {
70
+ if (spec[field] !== undefined && !isStringList(spec[field])) {
71
+ invalid(`\`${field}\` must be an array of non-empty strings.`);
72
+ }
73
+ }
74
+ const positional = spec.positionalOperandsBeforeCommand;
75
+ if (positional !== undefined && !(Number.isInteger(positional) && positional >= 0)) {
76
+ invalid('`positionalOperandsBeforeCommand` must be a non-negative integer.');
77
+ }
78
+ const assignment = spec.assignmentOperands;
79
+ if (assignment !== undefined && !ASSIGNMENT_OPERANDS.includes(assignment)) {
80
+ invalid(`\`assignmentOperands\` must be one of ${ASSIGNMENT_OPERANDS.join(', ')}.`);
81
+ }
82
+ return spec;
83
+ }
84
+ /**
85
+ * Reads `settings.sh.networkWrapper`: the configured name or `WrapperSpec`,
86
+ * or `undefined` when it is not set. Throws an error that starts
87
+ * `Invalid settings.sh.networkWrapper:` when the value is malformed. Call it
88
+ * from `create()`, so the error surfaces as ESLint's configuration message.
89
+ *
90
+ * @internal
91
+ */
92
+ export function readNetworkWrapperSetting(settings) {
93
+ const sh = settings.sh;
94
+ if (typeof sh !== 'object' || sh === null)
95
+ return undefined;
96
+ const value = sh.networkWrapper;
97
+ return value === undefined ? undefined : validate(value);
98
+ }
99
+ /**
100
+ * The `WrapperSpec` for the network wrapper set in `settings.sh.networkWrapper`,
101
+ * for a rule that looks through wrappers, or `null` when none is set. A name
102
+ * that is one of `sh-ast/analyze`'s default wrappers (`timeout`) keeps that
103
+ * entry's known options; any other name is a wrapper with no options.
104
+ *
105
+ * @internal
106
+ */
107
+ export function networkWrapperSpec(settings) {
108
+ const wrapper = readNetworkWrapperSetting(settings);
109
+ if (wrapper === undefined)
110
+ return null;
111
+ if (typeof wrapper !== 'string')
112
+ return wrapper;
113
+ return (DEFAULT_TRANSPARENT_WRAPPERS.find((spec) => spec.names.includes(wrapper)) ?? {
114
+ names: [wrapper],
115
+ });
116
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * A model of the shell's `set` option-flag syntax, shared by rules that ask
3
+ * "which shell options does this `set` command (or this shell invocation)
4
+ * turn on or off?".
5
+ *
6
+ * It knows two things and nothing else:
7
+ *
8
+ * - the option names `set -o` accepts in bash and POSIX sh, and the
9
+ * single-letter flag each one has (if any);
10
+ * - how `set` reads its arguments: `-abc` / `+abc` flag clusters, where each
11
+ * `o` in a cluster consumes the next argument as a long option name;
12
+ * `--` and a lone `-` end option processing; and the first argument that is
13
+ * not a flag cluster starts the positional parameters.
14
+ *
15
+ * It does not resolve words. Callers pass each argument's static text, or
16
+ * `null` for an argument whose text is not statically known; resolution is
17
+ * the `ShellSourceCode` helpers' job.
18
+ *
19
+ * @see https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
20
+ * @see https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#set
21
+ */
22
+ /** The long option names in {@link SET_OPTION_FLAGS}, in table order. @internal */
23
+ export declare const SET_OPTION_NAMES: readonly string[];
24
+ /**
25
+ * One option change a `set`-style argument list asks for: turn the named
26
+ * option on (`enable: true`, from `-x` / `-o name`) or off (`enable: false`,
27
+ * from `+x` / `+o name`).
28
+ *
29
+ * @internal
30
+ */
31
+ export interface SetOptionChange {
32
+ /** The long option name, e.g. `errexit`. */
33
+ readonly option: string;
34
+ /** `true` for a `-` flag, `false` for a `+` flag. */
35
+ readonly enable: boolean;
36
+ }
37
+ /**
38
+ * What {@link parseSetArguments} read from an argument list.
39
+ *
40
+ * @internal
41
+ */
42
+ export interface ParsedSetArguments {
43
+ /** The option changes, in the order the shell applies them. */
44
+ readonly changes: readonly SetOptionChange[];
45
+ /**
46
+ * Why option processing stopped before the end of the list, or `undefined`
47
+ * when every argument was an option flag (or an option name an `o` consumed):
48
+ *
49
+ * - `operand`: an argument that is not a flag cluster (or `--`, or a lone
50
+ * `-`) starts the positional parameters;
51
+ * - `unknown`: an argument's text is not statically known, so neither it nor
52
+ * anything after it can be read;
53
+ * - `malformed`: a cluster names a letter `set` does not have, or an `o` has
54
+ * no option name after it, or names an option `set` does not have.
55
+ */
56
+ readonly stoppedBy: 'operand' | 'unknown' | 'malformed' | undefined;
57
+ }
58
+ /**
59
+ * Reads a `set`-style argument list (the arguments after the command word, so
60
+ * `["-euo", "pipefail"]` for `set -euo pipefail`) into the option changes it
61
+ * makes, without applying them.
62
+ *
63
+ * Changes found before processing stops are kept: `set -e "$x"` still enables
64
+ * `errexit`, then stops (`unknown`) at `"$x"`, whose expansion could be
65
+ * anything. A lone `-` turns off `xtrace` and `verbose` (POSIX) and ends option
66
+ * processing, as `--` does. A lone `+` changes nothing and does not end
67
+ * option processing: bash and dash both read `set + -e` as `set -e`.
68
+ *
69
+ * @param args - Each argument's static text, or `null` when it is not static.
70
+ * @internal
71
+ */
72
+ export declare function parseSetArguments(args: readonly (string | null)[]): ParsedSetArguments;
73
+ /**
74
+ * Applies `changes` to `enabled` in order, so a later `+e` undoes an earlier
75
+ * `-e`. Mutates and returns `enabled`.
76
+ *
77
+ * @internal
78
+ */
79
+ export declare function applySetOptionChanges(enabled: Set<string>, changes: readonly SetOptionChange[]): Set<string>;
80
+ /**
81
+ * Spells a `set` command that enables exactly `options`, preferring each
82
+ * option's single-letter flag: the letters form one cluster (in the given
83
+ * order), the first letterless option is attached to that cluster through a
84
+ * trailing `o`, and any further letterless options follow as `-o <name>`.
85
+ * So `["errexit", "nounset", "pipefail"]` is `set -euo pipefail`.
86
+ *
87
+ * @param options - Long option names from {@link SET_OPTION_NAMES}; at least one.
88
+ * @internal
89
+ */
90
+ export declare function formatSetCommand(options: readonly string[]): string;
91
+ /**
92
+ * The option changes the shebang line passes to the shell (read by
93
+ * `require-strict-mode`'s `allowShebangOnly` and by
94
+ * `compat/no-nounset-empty-array`). Only spellings the kernel hands to the shell as written
95
+ * are read:
96
+ *
97
+ * - `#!/path/to/shell <word>`: the kernel passes everything after the
98
+ * interpreter as one argument, so exactly one flag word is read
99
+ * (`#!/bin/bash -eu`); more than one word (`#!/bin/bash -e -u`) arrives as a
100
+ * single argument the shell rejects, and counts as nothing.
101
+ * - `#!/usr/bin/env -S <shell> <words…>`: `env -S` splits the rest into words,
102
+ * so every word after the shell name is read.
103
+ *
104
+ * Plain `#!/usr/bin/env <shell> <flags>` counts as nothing: without `-S`,
105
+ * Linux passes `<shell> <flags>` to `env` as one command name.
106
+ *
107
+ * @internal
108
+ */
109
+ export declare function shebangOptionChanges(text: string): readonly SetOptionChange[];
@@ -0,0 +1,201 @@
1
+ /**
2
+ * A model of the shell's `set` option-flag syntax, shared by rules that ask
3
+ * "which shell options does this `set` command (or this shell invocation)
4
+ * turn on or off?".
5
+ *
6
+ * It knows two things and nothing else:
7
+ *
8
+ * - the option names `set -o` accepts in bash and POSIX sh, and the
9
+ * single-letter flag each one has (if any);
10
+ * - how `set` reads its arguments: `-abc` / `+abc` flag clusters, where each
11
+ * `o` in a cluster consumes the next argument as a long option name;
12
+ * `--` and a lone `-` end option processing; and the first argument that is
13
+ * not a flag cluster starts the positional parameters.
14
+ *
15
+ * It does not resolve words. Callers pass each argument's static text, or
16
+ * `null` for an argument whose text is not statically known; resolution is
17
+ * the `ShellSourceCode` helpers' job.
18
+ *
19
+ * @see https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
20
+ * @see https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#set
21
+ */
22
+ /**
23
+ * Every long option name `set -o` accepts in bash or POSIX sh, mapped to the
24
+ * single-letter flag that toggles the same option, or `undefined` when the
25
+ * option has no letter (and can only be spelled `-o <name>`).
26
+ *
27
+ * @internal
28
+ */
29
+ const SET_OPTION_FLAGS = Object.freeze({
30
+ allexport: 'a',
31
+ braceexpand: 'B',
32
+ emacs: undefined,
33
+ errexit: 'e',
34
+ errtrace: 'E',
35
+ functrace: 'T',
36
+ hashall: 'h',
37
+ histexpand: 'H',
38
+ history: undefined,
39
+ ignoreeof: undefined,
40
+ 'interactive-comments': undefined,
41
+ keyword: 'k',
42
+ monitor: 'm',
43
+ noclobber: 'C',
44
+ noexec: 'n',
45
+ noglob: 'f',
46
+ nolog: undefined,
47
+ notify: 'b',
48
+ nounset: 'u',
49
+ onecmd: 't',
50
+ physical: 'P',
51
+ pipefail: undefined,
52
+ posix: undefined,
53
+ privileged: 'p',
54
+ verbose: 'v',
55
+ vi: undefined,
56
+ xtrace: 'x',
57
+ });
58
+ /** The long option names in {@link SET_OPTION_FLAGS}, in table order. @internal */
59
+ export const SET_OPTION_NAMES = Object.freeze(Object.keys(SET_OPTION_FLAGS));
60
+ /** Single-letter flag → the long option name it toggles (the inverse of {@link SET_OPTION_FLAGS}). */
61
+ const OPTION_NAME_BY_FLAG = new Map(Object.entries(SET_OPTION_FLAGS).flatMap(([name, flag]) => flag === undefined ? [] : [[flag, name]]));
62
+ /**
63
+ * Reads a `set`-style argument list (the arguments after the command word, so
64
+ * `["-euo", "pipefail"]` for `set -euo pipefail`) into the option changes it
65
+ * makes, without applying them.
66
+ *
67
+ * Changes found before processing stops are kept: `set -e "$x"` still enables
68
+ * `errexit`, then stops (`unknown`) at `"$x"`, whose expansion could be
69
+ * anything. A lone `-` turns off `xtrace` and `verbose` (POSIX) and ends option
70
+ * processing, as `--` does. A lone `+` changes nothing and does not end
71
+ * option processing: bash and dash both read `set + -e` as `set -e`.
72
+ *
73
+ * @param args - Each argument's static text, or `null` when it is not static.
74
+ * @internal
75
+ */
76
+ export function parseSetArguments(args) {
77
+ const changes = [];
78
+ let index = 0;
79
+ while (index < args.length) {
80
+ const arg = args[index];
81
+ index += 1;
82
+ if (arg === null)
83
+ return { changes, stoppedBy: 'unknown' };
84
+ if (arg === '--')
85
+ return { changes, stoppedBy: undefined };
86
+ if (arg === '-') {
87
+ changes.push({ option: 'xtrace', enable: false }, { option: 'verbose', enable: false });
88
+ return { changes, stoppedBy: undefined };
89
+ }
90
+ if (arg === '+')
91
+ continue;
92
+ const sign = arg[0];
93
+ if ((sign !== '-' && sign !== '+') || arg.length < 2)
94
+ return { changes, stoppedBy: 'operand' };
95
+ const enable = sign === '-';
96
+ for (const flag of arg.slice(1)) {
97
+ if (flag === 'o') {
98
+ const name = index < args.length ? args[index] : undefined;
99
+ index += 1;
100
+ if (name === null)
101
+ return { changes, stoppedBy: 'unknown' };
102
+ if (name === undefined || !Object.hasOwn(SET_OPTION_FLAGS, name)) {
103
+ return { changes, stoppedBy: 'malformed' };
104
+ }
105
+ changes.push({ option: name, enable });
106
+ continue;
107
+ }
108
+ const option = OPTION_NAME_BY_FLAG.get(flag);
109
+ if (option === undefined)
110
+ return { changes, stoppedBy: 'malformed' };
111
+ changes.push({ option, enable });
112
+ }
113
+ }
114
+ return { changes, stoppedBy: undefined };
115
+ }
116
+ /**
117
+ * Applies `changes` to `enabled` in order, so a later `+e` undoes an earlier
118
+ * `-e`. Mutates and returns `enabled`.
119
+ *
120
+ * @internal
121
+ */
122
+ export function applySetOptionChanges(enabled, changes) {
123
+ for (const { option, enable } of changes) {
124
+ if (enable)
125
+ enabled.add(option);
126
+ else
127
+ enabled.delete(option);
128
+ }
129
+ return enabled;
130
+ }
131
+ /**
132
+ * Spells a `set` command that enables exactly `options`, preferring each
133
+ * option's single-letter flag: the letters form one cluster (in the given
134
+ * order), the first letterless option is attached to that cluster through a
135
+ * trailing `o`, and any further letterless options follow as `-o <name>`.
136
+ * So `["errexit", "nounset", "pipefail"]` is `set -euo pipefail`.
137
+ *
138
+ * @param options - Long option names from {@link SET_OPTION_NAMES}; at least one.
139
+ * @internal
140
+ */
141
+ export function formatSetCommand(options) {
142
+ const letters = options.flatMap((option) => SET_OPTION_FLAGS[option] ?? []).join('');
143
+ const longOnly = options.filter((option) => SET_OPTION_FLAGS[option] === undefined);
144
+ const words = ['set'];
145
+ let remaining = longOnly;
146
+ if (letters.length > 0) {
147
+ if (longOnly.length === 0) {
148
+ words.push(`-${letters}`);
149
+ }
150
+ else {
151
+ const [first, ...rest] = longOnly;
152
+ words.push(`-${letters}o`, first);
153
+ remaining = rest;
154
+ }
155
+ }
156
+ for (const option of remaining)
157
+ words.push('-o', option);
158
+ return words.join(' ');
159
+ }
160
+ /**
161
+ * The option changes the shebang line passes to the shell (read by
162
+ * `require-strict-mode`'s `allowShebangOnly` and by
163
+ * `compat/no-nounset-empty-array`). Only spellings the kernel hands to the shell as written
164
+ * are read:
165
+ *
166
+ * - `#!/path/to/shell <word>`: the kernel passes everything after the
167
+ * interpreter as one argument, so exactly one flag word is read
168
+ * (`#!/bin/bash -eu`); more than one word (`#!/bin/bash -e -u`) arrives as a
169
+ * single argument the shell rejects, and counts as nothing.
170
+ * - `#!/usr/bin/env -S <shell> <words…>`: `env -S` splits the rest into words,
171
+ * so every word after the shell name is read.
172
+ *
173
+ * Plain `#!/usr/bin/env <shell> <flags>` counts as nothing: without `-S`,
174
+ * Linux passes `<shell> <flags>` to `env` as one command name.
175
+ *
176
+ * @internal
177
+ */
178
+ export function shebangOptionChanges(text) {
179
+ if (!text.startsWith('#!'))
180
+ return [];
181
+ const lineEnd = text.search(/\r?\n|\r/u);
182
+ const words = text
183
+ .slice(2, lineEnd === -1 ? undefined : lineEnd)
184
+ .trim()
185
+ .split(/\s+/u);
186
+ const [interpreter = '', ...rest] = words;
187
+ const isEnv = interpreter === 'env' || interpreter.endsWith('/env');
188
+ let flagWords;
189
+ if (isEnv) {
190
+ const [split, , ...shellArgs] = rest;
191
+ if (split !== '-S' && split !== '--split-string')
192
+ return [];
193
+ flagWords = shellArgs;
194
+ }
195
+ else {
196
+ if (rest.length !== 1)
197
+ return [];
198
+ flagWords = rest;
199
+ }
200
+ return parseSetArguments(flagWords).changes;
201
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared reading of a shebang line's interpreter, so every rule that asks
3
+ * "which shell does this file name?" gets one answer instead of re-deriving it.
4
+ */
5
+ /**
6
+ * The shell basename a shebang's interpreter part (everything after `#!`)
7
+ * runs, or `undefined` when it names no usable interpreter: either an absolute
8
+ * path (`/usr/local/bin/bash` → `bash`), or an absolute path to `env` whose
9
+ * command is the shell (`/usr/bin/env bash`, `/usr/bin/env -S bash -e` →
10
+ * `bash`). A relative interpreter (`#!bash`) yields `undefined`: the kernel
11
+ * would resolve it against the caller's working directory, not `PATH`.
12
+ *
13
+ * Which names count as a shell, and whether one agrees with the configured
14
+ * dialect, are the callers' questions; this only answers "what runs".
15
+ *
16
+ * @internal
17
+ */
18
+ export declare function shebangShell(interpreterSpec: string): string | undefined;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Shared reading of a shebang line's interpreter, so every rule that asks
3
+ * "which shell does this file name?" gets one answer instead of re-deriving it.
4
+ */
5
+ /**
6
+ * `env` options that consume the following word as their argument
7
+ * (GNU coreutils and BSD spellings). Skipping that word keeps it from being
8
+ * mistaken for the command `env` runs.
9
+ */
10
+ const ENV_OPTIONS_WITH_ARGUMENT = new Set([
11
+ '-u',
12
+ '--unset',
13
+ '-C',
14
+ '--chdir',
15
+ '-P',
16
+ ]);
17
+ /** A `NAME=value` word, which `env` treats as an environment assignment. */
18
+ const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
19
+ /** The final path segment of `path` (`/usr/local/bin/bash` → `bash`). */
20
+ function basename(path) {
21
+ return path.slice(path.lastIndexOf('/') + 1);
22
+ }
23
+ /**
24
+ * The command an `env` shebang runs, given the words after `env`, or
25
+ * `undefined` when there is none. Options, their arguments, and `NAME=value`
26
+ * assignments are skipped; `--` ends the options, but assignments after it
27
+ * still apply (GNU `env -- FOO=1 sh` runs `sh`). `-S` (split string) is transparent here because
28
+ * the shebang's words are already split on whitespace, so `env -S bash -e`
29
+ * runs `bash`.
30
+ */
31
+ function envCommand(words) {
32
+ for (let index = 0; index < words.length; index++) {
33
+ const word = words[index];
34
+ if (word === '--') {
35
+ return words.slice(index + 1).find((operand) => !ENV_ASSIGNMENT.test(operand));
36
+ }
37
+ if (word === '-S' || word === '--split-string') {
38
+ continue;
39
+ }
40
+ if (word.startsWith('--split-string=')) {
41
+ return word.slice('--split-string='.length) || words[index + 1];
42
+ }
43
+ if (word.startsWith('-S')) {
44
+ return word.slice('-S'.length);
45
+ }
46
+ if (ENV_OPTIONS_WITH_ARGUMENT.has(word)) {
47
+ index++;
48
+ continue;
49
+ }
50
+ if (word.startsWith('-') || ENV_ASSIGNMENT.test(word)) {
51
+ continue;
52
+ }
53
+ return word;
54
+ }
55
+ return undefined;
56
+ }
57
+ /**
58
+ * The shell basename a shebang's interpreter part (everything after `#!`)
59
+ * runs, or `undefined` when it names no usable interpreter: either an absolute
60
+ * path (`/usr/local/bin/bash` → `bash`), or an absolute path to `env` whose
61
+ * command is the shell (`/usr/bin/env bash`, `/usr/bin/env -S bash -e` →
62
+ * `bash`). A relative interpreter (`#!bash`) yields `undefined`: the kernel
63
+ * would resolve it against the caller's working directory, not `PATH`.
64
+ *
65
+ * Which names count as a shell, and whether one agrees with the configured
66
+ * dialect, are the callers' questions; this only answers "what runs".
67
+ *
68
+ * @internal
69
+ */
70
+ export function shebangShell(interpreterSpec) {
71
+ const [interpreter, ...args] = interpreterSpec.trim().split(/\s+/u);
72
+ if (!interpreter.startsWith('/')) {
73
+ return undefined;
74
+ }
75
+ const name = basename(interpreter);
76
+ if (name !== 'env') {
77
+ return name;
78
+ }
79
+ const command = envCommand(args);
80
+ return command === undefined ? undefined : basename(command);
81
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The rule's report kinds, one per direction of the bash/POSIX mismatch.
3
+ *
4
+ * @internal
5
+ */
6
+ export type ShebangMatchesDialectMessageIds = 'posixShebangBashDialect' | 'bashShebangPosixDialect';
7
+ /**
8
+ * The rule module registered as `shebang-matches-dialect`.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const shebangMatchesDialect: import("./define-shell-rule.js").ShellRule<[], ShebangMatchesDialectMessageIds>;