@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,134 @@
1
+ /**
2
+ * How a POSIX-only compat rule finds a use of a command word POSIX sh lacks.
3
+ *
4
+ * Under `dialect: "posix"` the parser has no `[[` clause, `let` clause or
5
+ * similar: it reads `[[ a ]]`, `let n=1` and `source f` as ordinary commands,
6
+ * so the rules recognize them by the command they run. That reading is shared
7
+ * here so the three rules agree on what counts as a use:
8
+ *
9
+ * - **It is the effective command**, after following transparent wrappers,
10
+ * which is what actually runs (`command let n=1` fails on dash as `let n=1`
11
+ * does). `command -v let` asks a question instead of running `let`, and
12
+ * resolves to no such command.
13
+ * - **A shell without the construct can reach it.** These constructs are
14
+ * commands, not syntax, so a POSIX shell only fails when it runs one. A script
15
+ * meant for `sh` can hold a branch for one shell, and dash never enters it:
16
+ * `if [ -n "${BASH_VERSION-}" ]; then source ~/.bashrc; fi`. Such a use is not
17
+ * reported, because the finding's claim would be false.
18
+ *
19
+ * Recognizing that branch is deliberately shallow and syntactic, like the guard
20
+ * in `require-tty-guard`: no dataflow, and only the shapes {@link isGuarded}
21
+ * lists, each testing one of `BASH_VERSION`, `KSH_VERSION` or `ZSH_VERSION`
22
+ * directly. Anything else, including a version captured in another variable,
23
+ * reports.
24
+ */
25
+ import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
26
+ import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
27
+ /**
28
+ * The wrappers followed to find the command an invocation runs:
29
+ * `sh-ast/analyze`'s defaults (which include `command`) plus Bash's `builtin`.
30
+ */
31
+ const TRANSPARENT_WRAPPERS = [
32
+ ...DEFAULT_TRANSPARENT_WRAPPERS,
33
+ BUILTIN_WRAPPER,
34
+ ];
35
+ /** `sh-ast`'s `BinCmdOperator` codes for `&&` and `||`, which the lint view preserves. */
36
+ const AND_STMT = 11;
37
+ const OR_STMT = 12;
38
+ /**
39
+ * The source text of a word that reads a shell's own version variable: `$X`,
40
+ * `${X}`, `${X-}` or `${X:-}`, quoted or not. dash sets none of these variables.
41
+ */
42
+ const SHELL_VERSION_WORD = /^"?\$(?:\{(?:BASH|KSH|ZSH)_VERSION(?::?-)?\}|(?:BASH|KSH|ZSH)_VERSION)"?$/u;
43
+ /**
44
+ * What `stmt` tests, when it is exactly `[ -n V ]`, `test -n V` (`set`) or the
45
+ * `-z` forms (`unset`) for a version variable `V`, not negated. Anything else
46
+ * is `undefined`.
47
+ */
48
+ function shellVersionTest(sourceCode, stmt) {
49
+ if (stmt?.type !== 'Stmt' || stmt.negated || stmt.cmd?.type !== 'CallExpr')
50
+ return undefined;
51
+ const { args } = stmt.cmd;
52
+ const name = sourceCode.getCommandName(stmt.cmd);
53
+ if (args.length !== (name === '[' ? 4 : name === 'test' ? 3 : 0))
54
+ return undefined;
55
+ if (name === '[' && sourceCode.resolveWordText(args[3]) !== ']')
56
+ return undefined;
57
+ if (!SHELL_VERSION_WORD.test(sourceCode.getText(args[2])))
58
+ return undefined;
59
+ const operator = sourceCode.resolveWordText(args[1]);
60
+ if (operator === '-n')
61
+ return 'set';
62
+ return operator === '-z' ? 'unset' : undefined;
63
+ }
64
+ /**
65
+ * Whether `stmt` leaves the script or function unless a version variable is
66
+ * set: `[ -n V ] || exit` or `[ -z V ] && exit`, with `exit` or `return`.
67
+ */
68
+ function leavesUnlessVersioned(sourceCode, stmt) {
69
+ if (stmt.type !== 'Stmt' || stmt.cmd?.type !== 'BinaryCmd')
70
+ return false;
71
+ const { op, x, y } = stmt.cmd;
72
+ const test = shellVersionTest(sourceCode, x);
73
+ if (!((op === OR_STMT && test === 'set') || (op === AND_STMT && test === 'unset')))
74
+ return false;
75
+ if (y?.cmd?.type !== 'CallExpr')
76
+ return false;
77
+ const name = sourceCode.getCommandName(y.cmd);
78
+ return name === 'exit' || name === 'return';
79
+ }
80
+ /**
81
+ * Whether only a shell that sets a version variable can run `node`, because it
82
+ * sits in one of these:
83
+ *
84
+ * - the `then` body of `if [ -n V ]`, or the `else` body of `if [ -z V ]`;
85
+ * - the right side of `[ -n V ] && …` or `[ -z V ] || …`;
86
+ * - a statement list, after an earlier `[ -n V ] || exit` or `[ -z V ] && exit`
87
+ * in that list (the file, or a `{ … }` block such as a function body).
88
+ */
89
+ function isGuarded(sourceCode, node) {
90
+ let child = node;
91
+ for (let parent = sourceCode.getParent(child); parent; parent = sourceCode.getParent(child)) {
92
+ if (parent.type === 'IfClause' && parent.cond.length === 1) {
93
+ const test = shellVersionTest(sourceCode, parent.cond[0]);
94
+ if (test === 'set' && parent.then.some((stmt) => stmt === child))
95
+ return true;
96
+ if (test === 'unset' && parent.else === child)
97
+ return true;
98
+ }
99
+ else if (parent.type === 'BinaryCmd' && parent.y === child) {
100
+ const test = shellVersionTest(sourceCode, parent.x);
101
+ if ((parent.op === AND_STMT && test === 'set') ||
102
+ (parent.op === OR_STMT && test === 'unset')) {
103
+ return true;
104
+ }
105
+ }
106
+ else if (parent.type === 'File' || parent.type === 'Block') {
107
+ const index = parent.stmts.findIndex((stmt) => stmt === child);
108
+ if (parent.stmts.slice(0, index).some((stmt) => leavesUnlessVersioned(sourceCode, stmt))) {
109
+ return true;
110
+ }
111
+ }
112
+ child = parent;
113
+ }
114
+ return false;
115
+ }
116
+ /**
117
+ * The word that names `command` in `node`, when `command` is what `node`
118
+ * effectively runs and a shell without it can reach `node` (see the module
119
+ * comment), or `undefined` otherwise. Reporting this word rather than the whole
120
+ * command matters for `[[`: the parser ends that command at the first `&&` or
121
+ * `||`, so the node would cover an arbitrary part of the expression.
122
+ *
123
+ * @internal
124
+ */
125
+ export function reachableCommandWord(sourceCode, node, command) {
126
+ const invoked = sourceCode.resolveInvokedCommand(node, {
127
+ transparentWrappers: TRANSPARENT_WRAPPERS,
128
+ });
129
+ if (invoked?.effective.static !== true || invoked.effective.text !== command)
130
+ return undefined;
131
+ if (isGuarded(sourceCode, node))
132
+ return undefined;
133
+ return node.args[invoked.chainIndices.at(-1) ?? 0];
134
+ }
@@ -0,0 +1,9 @@
1
+ import type { ShellNode } from '../../language/syntax-view.js';
2
+ import type { ShellRuleContext, ShellRuleVisitor } from '../define-shell-rule.js';
3
+ /**
4
+ * Builds the listeners that call `report(word)` for each operand word of a
5
+ * `shopt -s` command whose static text is `option`.
6
+ *
7
+ * @internal
8
+ */
9
+ export declare function shoptEnableVisitor<MessageIds extends string>(context: ShellRuleContext<[], MessageIds>, option: string, report: (word: ShellNode) => void): ShellRuleVisitor;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Shared detection of `shopt -s NAME`, for the compat rules that gate one
3
+ * `shopt` option (`lastpipe`, `globstar`).
4
+ *
5
+ * What is read: a `shopt` command (after transparent wrappers such as
6
+ * `builtin`) whose option words, up to the first operand, include `-s`
7
+ * (possibly inside a cluster such as `-sq`). Its operands are the option
8
+ * names it turns on. `shopt -u` turns options off (so does a `-u` beside the
9
+ * `-s`), `shopt -o` names `set -o` options, and an operand that is not a
10
+ * static word cannot be read, so none of those count.
11
+ */
12
+ import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
13
+ import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
14
+ /**
15
+ * The wrappers followed to find the `shopt` command: `sh-ast/analyze`'s
16
+ * defaults (which include `command`) plus Bash's `builtin`.
17
+ */
18
+ const TRANSPARENT_WRAPPERS = [
19
+ ...DEFAULT_TRANSPARENT_WRAPPERS,
20
+ BUILTIN_WRAPPER,
21
+ ];
22
+ /**
23
+ * Builds the listeners that call `report(word)` for each operand word of a
24
+ * `shopt -s` command whose static text is `option`.
25
+ *
26
+ * @internal
27
+ */
28
+ export function shoptEnableVisitor(context, option, report) {
29
+ const { sourceCode } = context;
30
+ return {
31
+ CallExpr(node) {
32
+ const invoked = sourceCode.resolveInvokedCommand(node, {
33
+ transparentWrappers: TRANSPARENT_WRAPPERS,
34
+ });
35
+ if (invoked?.effective.static !== true || invoked.effective.text !== 'shopt')
36
+ return;
37
+ // `chainIndices` index `node.args` directly, so slice before filtering.
38
+ const words = node.args.slice((invoked.chainIndices.at(-1) ?? 0) + 1);
39
+ let enables = false;
40
+ let disables = false;
41
+ let namesShellOptions = false;
42
+ let index = 0;
43
+ for (; index < words.length; index++) {
44
+ const text = sourceCode.resolveWordText(words[index]);
45
+ if (text === null || !/^-./u.test(text))
46
+ break;
47
+ if (text === '--') {
48
+ index++;
49
+ break;
50
+ }
51
+ enables ||= text.includes('s');
52
+ disables ||= text.includes('u');
53
+ namesShellOptions ||= text.includes('o');
54
+ }
55
+ if (!enables || disables || namesShellOptions)
56
+ return;
57
+ for (const word of words.slice(index)) {
58
+ if (sourceCode.resolveWordText(word) === option)
59
+ report(word);
60
+ }
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The version-gate table every `compat/*` rule reads, and the one function
3
+ * that decides whether a gated feature is available to a file.
4
+ *
5
+ * A gate is data: a feature name mapped to the oldest Bash release that has
6
+ * it and whether POSIX sh has it. A new gate is a new row here, not new
7
+ * decision logic in a rule. The semantics of `languageOptions.shellVersion`
8
+ * and `languageOptions.dialect` for gating are defined once, by
9
+ * {@link isFeatureAvailable}, and documented for users in `docs/compat.md`.
10
+ *
11
+ * Every row is checked against real shells by `scripts/compat-matrix/run.sh`,
12
+ * which builds each Bash release and dash from source and runs one probe per
13
+ * feature on each; the observed results are the Evidence table in
14
+ * `docs/compat.md`. A changed row needs a probe there that shows the new
15
+ * boundary.
16
+ *
17
+ * @see https://tiswww.case.edu/php/chet/bash/NEWS — Bash release notes, the
18
+ * source of every minimum version below
19
+ * @see https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html
20
+ * — POSIX Shell Command Language: section 2.4 (reserved words) and section
21
+ * 2.9.1.4 (command search) leave the results of the `doubleBracket`,
22
+ * `letCommand` and `sourceCommand` rows unspecified
23
+ */
24
+ import type { ShellLanguageOptions } from '../../language/shell-language-options.js';
25
+ /**
26
+ * The name of a gated feature: a row of {@link FEATURE_GATES}.
27
+ *
28
+ * @internal
29
+ */
30
+ export type CompatFeature = 'appendAllRedirect' | 'associativeArrays' | 'braceSequencePaddingStep' | 'caseAttributes' | 'caseFallthrough' | 'caseModification' | 'coproc' | 'declareGlobal' | 'doubleBracket' | 'epochVariables' | 'fdVariable' | 'globstar' | 'lastpipe' | 'letCommand' | 'mapfile' | 'nameref' | 'negativeArrayIndex' | 'nounsetEmptyArray' | 'parameterTransformation' | 'parameterTransformationCase' | 'parameterTransformationKeys' | 'parameterTransformationQuotedKeys' | 'pipeAll' | 'printfTimeFormat' | 'sourceCommand' | 'testVariableSet' | 'unicodeEscape' | 'unicodeEscapeLatin1' | 'waitNext';
31
+ /**
32
+ * The two `languageOptions` fields gating reads. Both the raw and the
33
+ * normalized options satisfy it; an unset `dialect` means `"bash"`, as it
34
+ * does for the parser.
35
+ *
36
+ * @internal
37
+ */
38
+ export type GateLanguageOptions = Pick<ShellLanguageOptions, 'dialect' | 'shellVersion'>;
39
+ /**
40
+ * Whether `feature` is available to a file linted with `languageOptions`.
41
+ *
42
+ * - `dialect: "posix"`: available only when the gate's `posix` is `true`,
43
+ * at any `shellVersion` (a Bash-only feature is never available).
44
+ * - `dialect: "bash"` (the default) or `"bats"`: an unset `shellVersion`
45
+ * means the newest Bash, so every Bash feature is available. A set
46
+ * `shellVersion` makes the feature available when it is the gate's `bash`
47
+ * minimum or newer, comparing major then minor numerically (`"4"` is
48
+ * `4.0`; `"3.10"` is newer than `"3.2"`). A `null` minimum is available at
49
+ * every version.
50
+ * - `dialect: "mksh"` or `"zsh"`: the table carries no data for these
51
+ * shells, and their `shellVersion` numbers their own releases, not Bash's,
52
+ * so no gate applies and every feature is available.
53
+ *
54
+ * @internal
55
+ */
56
+ export declare function isFeatureAvailable(feature: CompatFeature, languageOptions: GateLanguageOptions): boolean;
57
+ /**
58
+ * Human-readable words for a gate's report: what the feature needs
59
+ * (`required`, such as `bash 4.0 or newer`) and what the file targets
60
+ * (`target`, such as `bash 3.2` or `POSIX sh`). Compat rule messages
61
+ * interpolate both as `{{required}}` and `{{target}}`.
62
+ *
63
+ * @internal
64
+ */
65
+ export interface GateDescription {
66
+ readonly required: string;
67
+ readonly target: string;
68
+ }
69
+ /**
70
+ * Describes why `feature` is unavailable under `languageOptions`, for a
71
+ * report's message data. Call it only when {@link isFeatureAvailable} is
72
+ * `false`.
73
+ *
74
+ * @internal
75
+ */
76
+ export declare function describeGate(feature: CompatFeature, languageOptions: GateLanguageOptions): GateDescription;
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Every gated feature, keyed by the name rules pass to
3
+ * {@link isFeatureAvailable}.
4
+ */
5
+ const FEATURE_GATES = Object.freeze({
6
+ /**
7
+ * The `&>>` redirection, appending both stdout and stderr: Bash 4.0 NEWS,
8
+ * "the new `&>>` redirection operator". Bash 3.2 rejects it as a syntax
9
+ * error, so the whole script fails to parse.
10
+ */
11
+ appendAllRedirect: Object.freeze({ bash: '4.0', posix: false }),
12
+ /** `declare -A` / `typeset -A`: Bash 4.0 NEWS, "associative arrays". */
13
+ associativeArrays: Object.freeze({ bash: '4.0', posix: false }),
14
+ /**
15
+ * Zero-padded (`{01..10}`) and stepped (`{1..10..2}`, `{a..e..2}`) brace
16
+ * sequences: Bash 4.0 NEWS (brace expansion gained zero-padding and an
17
+ * increment). Bash 3.2 expands the padded form without the padding and
18
+ * leaves the stepped form as a literal word; POSIX sh has no brace
19
+ * expansion.
20
+ */
21
+ braceSequencePaddingStep: Object.freeze({ bash: '4.0', posix: false }),
22
+ /**
23
+ * The case-modifying expansions `${v^}`, `${v^^}`, `${v,}`, `${v,,}`: Bash
24
+ * 4.0 NEWS, "new case-modifying word expansions".
25
+ */
26
+ caseModification: Object.freeze({ bash: '4.0', posix: false }),
27
+ /**
28
+ * The case-converting attributes `declare -l` / `-u` (and through
29
+ * `typeset` and `local`): Bash 4.0 NEWS, "new `-l` and `-u` options to
30
+ * declare/typeset/local".
31
+ */
32
+ caseAttributes: Object.freeze({ bash: '4.0', posix: false }),
33
+ /**
34
+ * The `case` terminators `;&` (fall through) and `;;&` (test the next
35
+ * pattern): Bash 4.0 NEWS. POSIX.1-2024 adds `;&`, but dash 0.5.12 still
36
+ * rejects it, so a POSIX target does not have it.
37
+ */
38
+ caseFallthrough: Object.freeze({ bash: '4.0', posix: false }),
39
+ /** The `coproc` reserved word: Bash 4.0 NEWS, "a new `coproc` reserved word". */
40
+ coproc: Object.freeze({ bash: '4.0', posix: false }),
41
+ /**
42
+ * `declare -g` / `typeset -g`: Bash 4.2 NEWS, "declare/typeset has a new
43
+ * `-g` option".
44
+ */
45
+ declareGlobal: Object.freeze({ bash: '4.2', posix: false }),
46
+ /**
47
+ * The `[[ … ]]` conditional. POSIX section 2.4 lists `[[` and `]]` among the
48
+ * words whose results are unspecified; every Bash has it, and dash runs it
49
+ * as a command that does not exist.
50
+ */
51
+ doubleBracket: Object.freeze({ bash: null, posix: false }),
52
+ /** `$EPOCHSECONDS` and `$EPOCHREALTIME`: Bash 5.0 NEWS. */
53
+ epochVariables: Object.freeze({ bash: '5.0', posix: false }),
54
+ /**
55
+ * A `{varname}` redirection that allocates a descriptor into a variable
56
+ * (`exec {fd}>file`): Bash 4.1 NEWS. Bash 4.0 runs `{fd}` as a command.
57
+ */
58
+ fdVariable: Object.freeze({ bash: '4.1', posix: false }),
59
+ /** `shopt -s globstar` (recursive `**`): Bash 4.0 NEWS. */
60
+ globstar: Object.freeze({ bash: '4.0', posix: false }),
61
+ /** `shopt -s lastpipe`: Bash 4.2 NEWS, "a new `lastpipe` shell option". */
62
+ lastpipe: Object.freeze({ bash: '4.2', posix: false }),
63
+ /**
64
+ * The `let` builtin. POSIX section 2.9.1.4 lists `let` among the command
65
+ * names whose results are unspecified; every Bash has it, and dash has no
66
+ * such command.
67
+ */
68
+ letCommand: Object.freeze({ bash: null, posix: false }),
69
+ /** The `mapfile` builtin and its `readarray` synonym: Bash 4.0 NEWS. */
70
+ mapfile: Object.freeze({ bash: '4.0', posix: false }),
71
+ /** Namerefs, `declare -n` / `typeset -n` / `local -n`: Bash 4.3 NEWS. */
72
+ nameref: Object.freeze({ bash: '4.3', posix: false }),
73
+ /**
74
+ * Negative indexed-array subscripts (`${a[-1]}`, `a[-1]=x`): Bash 4.3 NEWS,
75
+ * "assigning, referencing, and unsetting elements of indexed arrays using
76
+ * negative subscripts". Bash 4.2 already reads `${a[-1]}`, but rejects the
77
+ * element length `${#a[-1]}`, assignment and unset, so 4.3 is the first
78
+ * release where every use works.
79
+ */
80
+ negativeArrayIndex: Object.freeze({ bash: '4.3', posix: false }),
81
+ /**
82
+ * Expanding an empty array (`"${a[@]}"`, `${a[*]}`) while `set -u` is on:
83
+ * the syntax is in every Bash, but through 4.3 it aborts with "unbound
84
+ * variable"; Bash 4.4 is the first release where it expands to nothing.
85
+ */
86
+ nounsetEmptyArray: Object.freeze({ bash: '4.4', posix: false }),
87
+ /**
88
+ * The parameter transformations `${v@Q}`, `${v@E}`, `${v@P}`, `${v@A}` and
89
+ * `${v@a}`: Bash 4.4 NEWS.
90
+ */
91
+ parameterTransformation: Object.freeze({ bash: '4.4', posix: false }),
92
+ /** The case transformations `${v@U}`, `${v@u}` and `${v@L}`: Bash 5.1 NEWS. */
93
+ parameterTransformationCase: Object.freeze({ bash: '5.1', posix: false }),
94
+ /** `${a[@]@k}`, key-value pairs as separate words: Bash 5.2 NEWS. */
95
+ parameterTransformationKeys: Object.freeze({ bash: '5.2', posix: false }),
96
+ /** `${a[@]@K}`, quoted key-value pairs: Bash 5.1 NEWS. */
97
+ parameterTransformationQuotedKeys: Object.freeze({ bash: '5.1', posix: false }),
98
+ /**
99
+ * The `|&` pipe, shorthand for `2>&1 |`: Bash 4.0 NEWS. Bash 3.2 rejects
100
+ * it as a syntax error.
101
+ */
102
+ pipeAll: Object.freeze({ bash: '4.0', posix: false }),
103
+ /** `printf`'s `%(fmt)T` date format: Bash 4.2 NEWS. */
104
+ printfTimeFormat: Object.freeze({ bash: '4.2', posix: false }),
105
+ /**
106
+ * The `source` builtin, Bash's name for `.`. POSIX section 2.9.1.4 lists
107
+ * `source` among the command names whose results are unspecified; every Bash
108
+ * has it, and dash has no such command.
109
+ */
110
+ sourceCommand: Object.freeze({ bash: null, posix: false }),
111
+ /**
112
+ * The `-v name` test (`[[ -v x ]]`, `test -v x`, `[ -v x ]`): Bash 4.2
113
+ * NEWS. In older Bash, `[[ -v x ]]` is a syntax error.
114
+ */
115
+ testVariableSet: Object.freeze({ bash: '4.2', posix: false }),
116
+ /**
117
+ * The `\u`/`\U` escapes in `$'…'` strings: Bash 4.2 NEWS. Earlier releases
118
+ * leave the escape as literal text. See `unicodeEscapeLatin1` for the one
119
+ * range the 4.2 release decodes wrongly.
120
+ */
121
+ unicodeEscape: Object.freeze({ bash: '4.2', posix: false }),
122
+ /**
123
+ * A `\u`/`\U` escape for a code point from U+0080 to U+00FF: the Bash 4.2
124
+ * release tarball writes these as a single Latin-1 byte even in a UTF-8
125
+ * locale (observed: `e9` for U+00E9, where UTF-8 is `c3 a9`), while every
126
+ * other code point is UTF-8. Bash 4.3 is the first release that writes all
127
+ * of them as UTF-8.
128
+ */
129
+ unicodeEscapeLatin1: Object.freeze({ bash: '4.3', posix: false }),
130
+ /** `wait -n`, waiting for the next job to finish: Bash 4.3 NEWS. */
131
+ waitNext: Object.freeze({ bash: '4.3', posix: false }),
132
+ });
133
+ /**
134
+ * Splits a `MAJOR[.MINOR]` string into numbers, so `"4"` is `[4, 0]` and
135
+ * `"3.10"` is `[3, 10]`. Returns `undefined` for any other shape; the
136
+ * language's option validation already rejects those, so the gate never
137
+ * sees one from a real config.
138
+ */
139
+ function parseVersion(version) {
140
+ if (!/^\d+(?:\.\d+)?$/u.test(version))
141
+ return undefined;
142
+ const [major, minor = '0'] = version.split('.');
143
+ return [Number(major), Number(minor)];
144
+ }
145
+ /**
146
+ * Compares two `MAJOR[.MINOR]` versions numerically, major first, then minor:
147
+ * negative when `a` is older, `0` when equal, positive when `a` is newer. So
148
+ * `"3.10"` is newer than `"3.2"`, and `"4"` equals `"4.0"`.
149
+ */
150
+ function compareVersions(a, b) {
151
+ return a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1];
152
+ }
153
+ /**
154
+ * The dialects whose `shellVersion` is a Bash version. `bats` test files run
155
+ * under Bash, so they are gated exactly like `bash` files.
156
+ */
157
+ const BASH_DIALECTS = new Set(['bash', 'bats']);
158
+ /**
159
+ * Whether `feature` is available to a file linted with `languageOptions`.
160
+ *
161
+ * - `dialect: "posix"`: available only when the gate's `posix` is `true`,
162
+ * at any `shellVersion` (a Bash-only feature is never available).
163
+ * - `dialect: "bash"` (the default) or `"bats"`: an unset `shellVersion`
164
+ * means the newest Bash, so every Bash feature is available. A set
165
+ * `shellVersion` makes the feature available when it is the gate's `bash`
166
+ * minimum or newer, comparing major then minor numerically (`"4"` is
167
+ * `4.0`; `"3.10"` is newer than `"3.2"`). A `null` minimum is available at
168
+ * every version.
169
+ * - `dialect: "mksh"` or `"zsh"`: the table carries no data for these
170
+ * shells, and their `shellVersion` numbers their own releases, not Bash's,
171
+ * so no gate applies and every feature is available.
172
+ *
173
+ * @internal
174
+ */
175
+ export function isFeatureAvailable(feature, languageOptions) {
176
+ const gate = FEATURE_GATES[feature];
177
+ const dialect = languageOptions.dialect ?? 'bash';
178
+ if (dialect === 'posix')
179
+ return gate.posix;
180
+ if (!BASH_DIALECTS.has(dialect))
181
+ return true;
182
+ const { shellVersion } = languageOptions;
183
+ if (shellVersion === undefined || gate.bash === null)
184
+ return true;
185
+ const target = parseVersion(shellVersion);
186
+ const minimum = parseVersion(gate.bash);
187
+ if (target === undefined || minimum === undefined)
188
+ return true;
189
+ return compareVersions(target, minimum) >= 0;
190
+ }
191
+ /**
192
+ * Describes why `feature` is unavailable under `languageOptions`, for a
193
+ * report's message data. Call it only when {@link isFeatureAvailable} is
194
+ * `false`.
195
+ *
196
+ * @internal
197
+ */
198
+ export function describeGate(feature, languageOptions) {
199
+ const gate = FEATURE_GATES[feature];
200
+ const required = gate.bash === null ? 'bash' : `bash ${gate.bash} or newer`;
201
+ const target = (languageOptions.dialect ?? 'bash') === 'posix'
202
+ ? 'POSIX sh'
203
+ : `bash ${languageOptions.shellVersion ?? ''}`.trim();
204
+ return { required, target };
205
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The factory for this plugin's built-in rules: the public
3
+ * {@link defineShellRule} plus the one piece of metadata that only built-in
4
+ * rules share.
5
+ *
6
+ * `meta.docs.url` is derived from the rule name and always points at the
7
+ * rule's committed docs page, so the link a user follows from a lint report
8
+ * can never be hand-mistyped or left stale after a rename. Everything else —
9
+ * `meta.languages: ["sh/sh"]` and the shell-typed context, nodes and visitor —
10
+ * comes from `defineShellRule`, so built-in and third-party rules share one
11
+ * implementation of that guarantee.
12
+ *
13
+ * This module is internal (not re-exported from the package entry point): the
14
+ * docs URL scheme is this repository's. Rule packs use `defineShellRule` and
15
+ * write their own `meta.docs.url`.
16
+ */
17
+ import type { RulesMetaDocs } from '@eslint/core';
18
+ import type { ShellRule, ShellRuleContext, ShellRuleMeta, ShellRuleVisitor } from './define-shell-rule.js';
19
+ /**
20
+ * The canonical documentation URL for the built-in rule `name`. This is the
21
+ * single place the URL scheme is spelled; the factory uses it to fill
22
+ * `meta.docs.url`, and the docs-page tests use it to check each registered
23
+ * rule points at its own committed page.
24
+ *
25
+ * @internal
26
+ */
27
+ export declare function ruleDocsUrl(name: string): string;
28
+ /**
29
+ * The documentation URL for `page`, a path relative to this package's `docs/`
30
+ * directory (`wrapper-rules.md` is the committed file
31
+ * `packages/eslint-plugin/docs/wrapper-rules.md`). Used for rules that are not
32
+ * in the registry, which have no `docs/rules/<name>.md` page of their own.
33
+ *
34
+ * @internal
35
+ */
36
+ export declare function packageDocsUrl(page: string): string;
37
+ /**
38
+ * The `meta.docs` an author writes: everything ESLint accepts except `url`,
39
+ * which the factory derives from the rule name. Typed `never` so supplying a
40
+ * URL by hand is a compile error rather than a silently-overwritten value.
41
+ *
42
+ * @internal
43
+ */
44
+ export type BuiltInRuleDocs = Omit<RulesMetaDocs, 'url'> & {
45
+ readonly url?: never;
46
+ };
47
+ /**
48
+ * The `meta` a built-in rule writes: {@link ShellRuleMeta} (so the language
49
+ * fields are already forbidden), with `docs` narrowed to
50
+ * {@link BuiltInRuleDocs} because the factory derives `docs.url`.
51
+ *
52
+ * @internal
53
+ */
54
+ export type BuiltInRuleMeta<RuleOptions extends unknown[], MessageIds extends string> = Omit<ShellRuleMeta<RuleOptions, MessageIds>, 'docs'> & {
55
+ readonly docs?: BuiltInRuleDocs;
56
+ };
57
+ /**
58
+ * What an author hands to {@link createRule}: the rule's registry name (used
59
+ * to derive its docs URL), its author-owned metadata, and its `create()`.
60
+ *
61
+ * @internal
62
+ */
63
+ export interface BuiltInRuleSpec<RuleOptions extends unknown[], MessageIds extends string> {
64
+ /**
65
+ * The rule's name without the plugin namespace (`require-shebang`, not
66
+ * `sh/require-shebang`). For a built-in rule it must equal the rule's key in
67
+ * the registry and the basename of its docs page.
68
+ */
69
+ readonly name: string;
70
+ /**
71
+ * The docs URL of a rule that is not in the registry. A rule made by one of
72
+ * this package's public factories (for example `createWrapperRule`) has no
73
+ * `docs/rules/<name>.md` page: it links its factory's page
74
+ * ({@link packageDocsUrl}), or the page its rule pack supplies. Omit it for
75
+ * a built-in rule, whose page is always `docs/rules/<name>.md`.
76
+ */
77
+ readonly docsUrl?: string;
78
+ readonly meta: BuiltInRuleMeta<RuleOptions, MessageIds>;
79
+ /** Builds the rule's listeners for one file; receives the shell-typed context. */
80
+ readonly create: (context: ShellRuleContext<RuleOptions, MessageIds>) => ShellRuleVisitor;
81
+ }
82
+ /**
83
+ * Builds a built-in shell rule from its spec with {@link defineShellRule},
84
+ * after setting `meta.docs.url` to the rule's docs page ({@link ruleDocsUrl},
85
+ * or the spec's `docsUrl` for a rule outside the registry). The URL is
86
+ * written after the author's `docs`, so even an untyped caller cannot
87
+ * override it; `defineShellRule` does the same for `meta.languages`.
88
+ *
89
+ * @internal
90
+ */
91
+ export declare function createRule<RuleOptions extends unknown[] = [], MessageIds extends string = string>(spec: BuiltInRuleSpec<RuleOptions, MessageIds>): ShellRule<RuleOptions, MessageIds>;
@@ -0,0 +1,51 @@
1
+ import { defineShellRule } from './define-shell-rule.js';
2
+ /**
3
+ * Where this package's shipped docs live in the source repository: the
4
+ * committed `packages/eslint-plugin/docs/` directory.
5
+ */
6
+ const PACKAGE_DOCS_BASE_URL = 'https://github.com/mike-north/eslint-sh/blob/main/packages/eslint-plugin/docs/';
7
+ /**
8
+ * Where built-in rule docs pages live in the source repository. A rule named
9
+ * `foo` is documented at `<this>/foo.md`, i.e. the committed file
10
+ * `packages/eslint-plugin/docs/rules/foo.md`.
11
+ */
12
+ const RULE_DOCS_BASE_URL = `${PACKAGE_DOCS_BASE_URL}rules/`;
13
+ /**
14
+ * The canonical documentation URL for the built-in rule `name`. This is the
15
+ * single place the URL scheme is spelled; the factory uses it to fill
16
+ * `meta.docs.url`, and the docs-page tests use it to check each registered
17
+ * rule points at its own committed page.
18
+ *
19
+ * @internal
20
+ */
21
+ export function ruleDocsUrl(name) {
22
+ return `${RULE_DOCS_BASE_URL}${name}.md`;
23
+ }
24
+ /**
25
+ * The documentation URL for `page`, a path relative to this package's `docs/`
26
+ * directory (`wrapper-rules.md` is the committed file
27
+ * `packages/eslint-plugin/docs/wrapper-rules.md`). Used for rules that are not
28
+ * in the registry, which have no `docs/rules/<name>.md` page of their own.
29
+ *
30
+ * @internal
31
+ */
32
+ export function packageDocsUrl(page) {
33
+ return `${PACKAGE_DOCS_BASE_URL}${page}`;
34
+ }
35
+ /**
36
+ * Builds a built-in shell rule from its spec with {@link defineShellRule},
37
+ * after setting `meta.docs.url` to the rule's docs page ({@link ruleDocsUrl},
38
+ * or the spec's `docsUrl` for a rule outside the registry). The URL is
39
+ * written after the author's `docs`, so even an untyped caller cannot
40
+ * override it; `defineShellRule` does the same for `meta.languages`.
41
+ *
42
+ * @internal
43
+ */
44
+ export function createRule(spec) {
45
+ const { name, docsUrl, meta, create } = spec;
46
+ const url = docsUrl ?? ruleDocsUrl(name);
47
+ return defineShellRule({
48
+ meta: { ...meta, docs: { ...meta.docs, url } },
49
+ create,
50
+ });
51
+ }