@eslint-sh/eslint-plugin 0.5.0 β†’ 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +223 -17
  2. package/dist/command-harvest/builtins/bash.d.ts +36 -0
  3. package/dist/command-harvest/builtins/bash.js +98 -0
  4. package/dist/command-harvest/builtins/bats.d.ts +45 -0
  5. package/dist/command-harvest/builtins/bats.js +33 -0
  6. package/dist/command-harvest/builtins/index.d.ts +18 -0
  7. package/dist/command-harvest/builtins/index.js +39 -0
  8. package/dist/command-harvest/builtins/posix.d.ts +51 -0
  9. package/dist/command-harvest/builtins/posix.js +93 -0
  10. package/dist/command-harvest/harvest-commands.d.ts +66 -0
  11. package/dist/command-harvest/harvest-commands.js +97 -0
  12. package/dist/command-harvest/wrapper-table.d.ts +24 -0
  13. package/dist/command-harvest/wrapper-table.js +49 -0
  14. package/dist/configs/agent-authored.d.ts +41 -0
  15. package/dist/configs/agent-authored.js +33 -0
  16. package/dist/configs/bash-3-2.d.ts +28 -0
  17. package/dist/configs/bash-3-2.js +50 -0
  18. package/dist/configs/posix-portable.d.ts +40 -0
  19. package/dist/configs/posix-portable.js +52 -0
  20. package/dist/configs/recommended.d.ts +52 -0
  21. package/dist/configs/recommended.js +34 -0
  22. package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
  23. package/dist/declaration-conventions/comment-pragma.js +153 -0
  24. package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
  25. package/dist/declaration-conventions/declaration-convention.js +1 -0
  26. package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
  27. package/dist/declaration-conventions/read-declared-commands.js +128 -0
  28. package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
  29. package/dist/eslint-plugin-syntax.d.ts +27 -0
  30. package/dist/eslint-plugin.d.ts +1211 -52
  31. package/dist/index.d.ts +56 -8
  32. package/dist/index.js +53 -6
  33. package/dist/language/command-resolution.d.ts +124 -0
  34. package/dist/language/command-resolution.js +120 -0
  35. package/dist/language/end-of-input-marker.d.ts +29 -0
  36. package/dist/language/end-of-input-marker.js +88 -0
  37. package/dist/language/line-breaks.d.ts +18 -0
  38. package/dist/language/line-breaks.js +22 -0
  39. package/dist/language/lint-view-types.d.ts +104 -0
  40. package/dist/language/lint-view-types.js +1 -0
  41. package/dist/language/non-shell-file-guard.d.ts +35 -0
  42. package/dist/language/non-shell-file-guard.js +59 -0
  43. package/dist/language/selector-classes.d.ts +0 -1
  44. package/dist/language/selector-classes.js +0 -1
  45. package/dist/language/shell-language-options.d.ts +7 -1
  46. package/dist/language/shell-language-options.js +4 -4
  47. package/dist/language/shell-language.d.ts +10 -2
  48. package/dist/language/shell-language.js +59 -8
  49. package/dist/language/shell-source-code.d.ts +342 -5
  50. package/dist/language/shell-source-code.js +559 -30
  51. package/dist/language/source-coordinates.d.ts +13 -2
  52. package/dist/language/source-coordinates.js +68 -14
  53. package/dist/language/syntax-view.d.ts +60 -39
  54. package/dist/language/syntax-view.js +200 -45
  55. package/dist/language/word-resolution.d.ts +4 -3
  56. package/dist/language/word-resolution.js +4 -3
  57. package/dist/rules/compat/create-compat-rule.d.ts +59 -0
  58. package/dist/rules/compat/create-compat-rule.js +65 -0
  59. package/dist/rules/compat/declaration-options.d.ts +19 -0
  60. package/dist/rules/compat/declaration-options.js +110 -0
  61. package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
  62. package/dist/rules/compat/no-append-all-redirect.js +73 -0
  63. package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
  64. package/dist/rules/compat/no-associative-arrays.js +39 -0
  65. package/dist/rules/compat/no-case-attributes.d.ts +12 -0
  66. package/dist/rules/compat/no-case-attributes.js +63 -0
  67. package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
  68. package/dist/rules/compat/no-case-fallthrough.js +67 -0
  69. package/dist/rules/compat/no-case-modification.d.ts +35 -0
  70. package/dist/rules/compat/no-case-modification.js +86 -0
  71. package/dist/rules/compat/no-coproc.d.ts +12 -0
  72. package/dist/rules/compat/no-coproc.js +47 -0
  73. package/dist/rules/compat/no-declare-g.d.ts +12 -0
  74. package/dist/rules/compat/no-declare-g.js +59 -0
  75. package/dist/rules/compat/no-double-bracket.d.ts +12 -0
  76. package/dist/rules/compat/no-double-bracket.js +52 -0
  77. package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
  78. package/dist/rules/compat/no-epoch-variables.js +57 -0
  79. package/dist/rules/compat/no-fd-variable.d.ts +12 -0
  80. package/dist/rules/compat/no-fd-variable.js +48 -0
  81. package/dist/rules/compat/no-globstar.d.ts +12 -0
  82. package/dist/rules/compat/no-globstar.js +39 -0
  83. package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
  84. package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
  85. package/dist/rules/compat/no-let.d.ts +12 -0
  86. package/dist/rules/compat/no-let.js +62 -0
  87. package/dist/rules/compat/no-mapfile.d.ts +13 -0
  88. package/dist/rules/compat/no-mapfile.js +122 -0
  89. package/dist/rules/compat/no-nameref.d.ts +12 -0
  90. package/dist/rules/compat/no-nameref.js +44 -0
  91. package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
  92. package/dist/rules/compat/no-negative-array-index.js +123 -0
  93. package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
  94. package/dist/rules/compat/no-nounset-empty-array.js +158 -0
  95. package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
  96. package/dist/rules/compat/no-parameter-transformation.js +123 -0
  97. package/dist/rules/compat/no-pipe-all.d.ts +13 -0
  98. package/dist/rules/compat/no-pipe-all.js +68 -0
  99. package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
  100. package/dist/rules/compat/no-printf-time-format.js +99 -0
  101. package/dist/rules/compat/no-source.d.ts +13 -0
  102. package/dist/rules/compat/no-source.js +103 -0
  103. package/dist/rules/compat/no-test-v.d.ts +12 -0
  104. package/dist/rules/compat/no-test-v.js +280 -0
  105. package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
  106. package/dist/rules/compat/no-unicode-escape.js +120 -0
  107. package/dist/rules/compat/no-wait-n.d.ts +12 -0
  108. package/dist/rules/compat/no-wait-n.js +73 -0
  109. package/dist/rules/compat/operator-range.d.ts +10 -0
  110. package/dist/rules/compat/operator-range.js +15 -0
  111. package/dist/rules/compat/posix-absent-command.d.ts +12 -0
  112. package/dist/rules/compat/posix-absent-command.js +134 -0
  113. package/dist/rules/compat/shopt-options.d.ts +9 -0
  114. package/dist/rules/compat/shopt-options.js +63 -0
  115. package/dist/rules/compat/version-gates.d.ts +76 -0
  116. package/dist/rules/compat/version-gates.js +197 -0
  117. package/dist/rules/create-rule.d.ts +91 -0
  118. package/dist/rules/create-rule.js +51 -0
  119. package/dist/rules/create-wrapper-rule.d.ts +166 -0
  120. package/dist/rules/create-wrapper-rule.js +407 -0
  121. package/dist/rules/define-shell-rule.d.ts +184 -0
  122. package/dist/rules/define-shell-rule.js +56 -0
  123. package/dist/rules/index.d.ts +26 -0
  124. package/dist/rules/index.js +88 -0
  125. package/dist/rules/max-script-complexity.d.ts +28 -0
  126. package/dist/rules/max-script-complexity.js +130 -0
  127. package/dist/rules/no-dynamic-invocation.d.ts +35 -0
  128. package/dist/rules/no-dynamic-invocation.js +117 -0
  129. package/dist/rules/no-eval.d.ts +13 -0
  130. package/dist/rules/no-eval.js +89 -0
  131. package/dist/rules/no-masked-exit-status.d.ts +21 -0
  132. package/dist/rules/no-masked-exit-status.js +667 -0
  133. package/dist/rules/no-secret-env-reads.d.ts +24 -0
  134. package/dist/rules/no-secret-env-reads.js +133 -0
  135. package/dist/rules/no-secret-flags.d.ts +32 -0
  136. package/dist/rules/no-secret-flags.js +162 -0
  137. package/dist/rules/no-unchecked-cd.d.ts +44 -0
  138. package/dist/rules/no-unchecked-cd.js +250 -0
  139. package/dist/rules/no-undeclared-commands.d.ts +20 -0
  140. package/dist/rules/no-undeclared-commands.js +115 -0
  141. package/dist/rules/no-unused-declarations.d.ts +23 -0
  142. package/dist/rules/no-unused-declarations.js +259 -0
  143. package/dist/rules/prefer-local.d.ts +16 -0
  144. package/dist/rules/prefer-local.js +249 -0
  145. package/dist/rules/quote-expansions.d.ts +13 -0
  146. package/dist/rules/quote-expansions.js +229 -0
  147. package/dist/rules/require-network-wrapper.d.ts +40 -0
  148. package/dist/rules/require-network-wrapper.js +244 -0
  149. package/dist/rules/require-shebang.d.ts +29 -0
  150. package/dist/rules/require-shebang.js +101 -0
  151. package/dist/rules/require-strict-mode.d.ts +52 -0
  152. package/dist/rules/require-strict-mode.js +177 -0
  153. package/dist/rules/require-tty-guard.d.ts +13 -0
  154. package/dist/rules/require-tty-guard.js +289 -0
  155. package/dist/rules/resolve-declaration-convention.d.ts +96 -0
  156. package/dist/rules/resolve-declaration-convention.js +109 -0
  157. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  158. package/dist/rules/resolve-network-wrapper.js +116 -0
  159. package/dist/rules/set-options.d.ts +109 -0
  160. package/dist/rules/set-options.js +201 -0
  161. package/dist/rules/shebang-interpreter.d.ts +18 -0
  162. package/dist/rules/shebang-interpreter.js +81 -0
  163. package/dist/rules/shebang-matches-dialect.d.ts +12 -0
  164. package/dist/rules/shebang-matches-dialect.js +69 -0
  165. package/dist/rules/transparent-wrappers.d.ts +52 -0
  166. package/dist/rules/transparent-wrappers.js +99 -0
  167. package/dist/rules/user-pattern.d.ts +63 -0
  168. package/dist/rules/user-pattern.js +184 -0
  169. package/dist/syntax/analyze.d.ts +19 -0
  170. package/dist/syntax/analyze.js +21 -0
  171. package/dist/syntax/index.d.ts +23 -0
  172. package/dist/syntax/index.js +25 -0
  173. package/dist/version.d.ts +0 -1
  174. package/dist/version.js +0 -1
  175. package/docs/compat.md +347 -0
  176. package/docs/custom-rule-tutorial.md +780 -34
  177. package/docs/declaration-conventions.md +478 -0
  178. package/docs/examples/audit-wrapper-pack.mjs +15 -0
  179. package/docs/examples/dep-line-convention.mjs +22 -0
  180. package/docs/examples/eslint.config.mjs +3 -2
  181. package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
  182. package/docs/examples/no-backticks-rule-tester.mjs +106 -0
  183. package/docs/examples/no-backticks-rule.mjs +60 -0
  184. package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
  185. package/docs/examples/no-function-keyword-rule.mjs +59 -0
  186. package/docs/examples/no-listed-commands-rule.ts +63 -0
  187. package/docs/examples/no-rm-rf-rule.mjs +26 -15
  188. package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
  189. package/docs/examples/sidecar-convention.mjs +38 -0
  190. package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
  191. package/docs/examples/sorted-declarations-rule.mjs +52 -0
  192. package/docs/reading-nodes.md +159 -0
  193. package/docs/rules/README.md +78 -0
  194. package/docs/rules/_template.md +83 -0
  195. package/docs/rules/compat/no-append-all-redirect.md +87 -0
  196. package/docs/rules/compat/no-associative-arrays.md +95 -0
  197. package/docs/rules/compat/no-case-attributes.md +82 -0
  198. package/docs/rules/compat/no-case-fallthrough.md +90 -0
  199. package/docs/rules/compat/no-case-modification.md +74 -0
  200. package/docs/rules/compat/no-coproc.md +76 -0
  201. package/docs/rules/compat/no-declare-g.md +80 -0
  202. package/docs/rules/compat/no-double-bracket.md +72 -0
  203. package/docs/rules/compat/no-epoch-variables.md +79 -0
  204. package/docs/rules/compat/no-fd-variable.md +78 -0
  205. package/docs/rules/compat/no-globstar.md +74 -0
  206. package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
  207. package/docs/rules/compat/no-let.md +78 -0
  208. package/docs/rules/compat/no-mapfile.md +118 -0
  209. package/docs/rules/compat/no-nameref.md +81 -0
  210. package/docs/rules/compat/no-negative-array-index.md +87 -0
  211. package/docs/rules/compat/no-nounset-empty-array.md +103 -0
  212. package/docs/rules/compat/no-parameter-transformation.md +94 -0
  213. package/docs/rules/compat/no-pipe-all.md +85 -0
  214. package/docs/rules/compat/no-printf-time-format.md +75 -0
  215. package/docs/rules/compat/no-source.md +98 -0
  216. package/docs/rules/compat/no-test-v.md +82 -0
  217. package/docs/rules/compat/no-unicode-escape.md +91 -0
  218. package/docs/rules/compat/no-wait-n.md +85 -0
  219. package/docs/rules/max-script-complexity.md +109 -0
  220. package/docs/rules/no-dynamic-invocation.md +139 -0
  221. package/docs/rules/no-eval.md +106 -0
  222. package/docs/rules/no-masked-exit-status.md +239 -0
  223. package/docs/rules/no-secret-env-reads.md +173 -0
  224. package/docs/rules/no-secret-flags.md +169 -0
  225. package/docs/rules/no-unchecked-cd.md +146 -0
  226. package/docs/rules/no-undeclared-commands.md +141 -0
  227. package/docs/rules/no-unused-declarations.md +216 -0
  228. package/docs/rules/prefer-local.md +221 -0
  229. package/docs/rules/quote-expansions.md +157 -0
  230. package/docs/rules/require-network-wrapper.md +171 -0
  231. package/docs/rules/require-shebang.md +157 -0
  232. package/docs/rules/require-strict-mode.md +253 -0
  233. package/docs/rules/require-tty-guard.md +128 -0
  234. package/docs/rules/shebang-matches-dialect.md +120 -0
  235. package/docs/selectors.md +2 -2
  236. package/docs/shellcheck.md +86 -0
  237. package/docs/wrapper-rules.md +206 -0
  238. package/package.json +26 -20
  239. package/dist/index.d.ts.map +0 -1
  240. package/dist/index.js.map +0 -1
  241. package/dist/language/selector-classes.d.ts.map +0 -1
  242. package/dist/language/selector-classes.js.map +0 -1
  243. package/dist/language/shell-language-options.d.ts.map +0 -1
  244. package/dist/language/shell-language-options.js.map +0 -1
  245. package/dist/language/shell-language.d.ts.map +0 -1
  246. package/dist/language/shell-language.js.map +0 -1
  247. package/dist/language/shell-source-code.d.ts.map +0 -1
  248. package/dist/language/shell-source-code.js.map +0 -1
  249. package/dist/language/source-coordinates.d.ts.map +0 -1
  250. package/dist/language/source-coordinates.js.map +0 -1
  251. package/dist/language/syntax-view.d.ts.map +0 -1
  252. package/dist/language/syntax-view.js.map +0 -1
  253. package/dist/language/word-resolution.d.ts.map +0 -1
  254. package/dist/language/word-resolution.js.map +0 -1
  255. package/dist/version.d.ts.map +0 -1
  256. package/dist/version.js.map +0 -1
@@ -1,5 +1,10 @@
1
1
  import { ConfigCommentParser, Directive, TextSourceCodeBase, VisitNodeStep, } from '@eslint/plugin-kit';
2
- import { isShellComment, isShellNode, shellVisitorKeys, walkShell } from './syntax-view.js';
2
+ import { childNodes, isShellComment, projectedComments, projectedCoordinates, syntaxForNode, viewForSyntax, walkShell, } from './syntax-view.js';
3
+ import { SourceCoordinates } from './source-coordinates.js';
4
+ import { SYNTAX_INTERFACES, printSync } from 'sh-ast';
5
+ import { resolveArgv0 } from 'sh-ast/analyze';
6
+ import { buildCommandSiteIndex, rawCallExprFor, viewCommandSites } from './command-resolution.js';
7
+ import { resolveWord } from './word-resolution.js';
3
8
  /**
4
9
  * Shared instance β€” `ConfigCommentParser` is stateless (see
5
10
  * `@eslint/plugin-kit`'s implementation), so every `ShellSourceCode`
@@ -39,12 +44,9 @@ function buildDirectiveLabels(directivePrefix) {
39
44
  * call: the leading `#` is already excluded, e.g. the source
40
45
  * `# eslint-disable-next-line sh/foo -- reason` produces
41
46
  * `comment.text === " eslint-disable-next-line sh/foo -- reason"`).
42
- * `ShNode`'s index signature types every field `unknown`, so this narrows
43
- * defensively rather than assuming the shape.
44
47
  */
45
48
  function getCommentText(comment) {
46
- const { text } = comment;
47
- return typeof text === 'string' ? text : '';
49
+ return comment.text;
48
50
  }
49
51
  /**
50
52
  * Length of the line terminator starting at `text[index]` β€” `2` for `\r\n`,
@@ -179,6 +181,75 @@ function computeUnusedDirectiveFixRange(comment, text) {
179
181
  }
180
182
  return [start, end + newlineLength];
181
183
  }
184
+ /**
185
+ * Returns the byte offset of `pos` when it names an exact boundary the parser
186
+ * observed, and throws otherwise. Invalid, recovered, and overflowed positions
187
+ * carry no exact offset (upstream may report zero or a saturated value), so
188
+ * converting them would place a report or fix somewhere the parser never
189
+ * pointed. They are rejected rather than rounded to a nearby boundary.
190
+ *
191
+ * @throws TypeError when `pos` is not shaped like an sh-ast `SyntaxPos`.
192
+ * @throws RangeError when `pos` is invalid, recovered, or overflowed.
193
+ */
194
+ function exactByteOffset(pos) {
195
+ const candidate = pos;
196
+ if (typeof candidate !== 'object' ||
197
+ candidate === null ||
198
+ typeof pos.Offset !== 'number' ||
199
+ typeof pos.IsValid !== 'boolean' ||
200
+ typeof pos.IsRecovered !== 'boolean' ||
201
+ typeof pos.OffsetOverflow !== 'boolean') {
202
+ throw new TypeError('expected an sh-ast SyntaxPos');
203
+ }
204
+ if (!pos.IsValid || pos.IsRecovered || pos.OffsetOverflow) {
205
+ throw new RangeError(`syntax position has no exact source offset (IsValid: ${String(pos.IsValid)}, ` +
206
+ `IsRecovered: ${String(pos.IsRecovered)}, OffsetOverflow: ${String(pos.OffsetOverflow)})`);
207
+ }
208
+ return pos.Offset;
209
+ }
210
+ /**
211
+ * The record types `printSync` accepts as a root: `File`, `Stmt`, `Word`,
212
+ * `Assign`, every `Command` and every `WordPart` (sh-ast's documented
213
+ * supported roots). Any other record makes upstream's printer fail, so
214
+ * {@link ShellSourceCode.getPrintedReplacement} rejects it up front with a
215
+ * `TypeError` that names the type instead of surfacing a printer error.
216
+ */
217
+ const printableRootTypes = new Set([
218
+ 'File',
219
+ 'Stmt',
220
+ 'Word',
221
+ 'Assign',
222
+ ...SYNTAX_INTERFACES.Command,
223
+ ...SYNTAX_INTERFACES.WordPart,
224
+ ]);
225
+ /**
226
+ * Returns `edited`'s record type when it is a printable root, and throws a
227
+ * `TypeError` naming the type otherwise. `edited` is checked as an unknown
228
+ * value because rule code can pass anything.
229
+ */
230
+ function printableRootType(edited) {
231
+ const candidate = edited;
232
+ if (typeof candidate !== 'object' ||
233
+ candidate === null ||
234
+ !('Type' in candidate) ||
235
+ typeof candidate.Type !== 'string') {
236
+ throw new TypeError('expected an sh-ast syntax record to print');
237
+ }
238
+ if (!printableRootTypes.has(candidate.Type)) {
239
+ throw new TypeError(`cannot print a ${candidate.Type} as a fix: printSync accepts File, Stmt, Word, Assign, ` +
240
+ 'Command and WordPart roots');
241
+ }
242
+ return candidate.Type;
243
+ }
244
+ /**
245
+ * Whether the printer writes `stmt`'s terminator itself. A background (`&`),
246
+ * coprocess (`|&`) or disowned statement prints its own operator; any other
247
+ * statement prints with no terminator at all, because upstream leaves `;` to
248
+ * the enclosing statement list.
249
+ */
250
+ function printsOwnTerminator(stmt) {
251
+ return stmt.Background || stmt.Coprocess || stmt.Disown;
252
+ }
182
253
  /**
183
254
  * Wraps `comment` in a shallow copy whose `.range` is
184
255
  * {@link computeUnusedDirectiveFixRange}'s (possibly widened) range, while
@@ -209,8 +280,8 @@ function withUnusedDirectiveFixRange(comment, text) {
209
280
  * `applyInlineConfig`) recognizes all four disable-comment forms
210
281
  * (`<prefix>-disable`, `<prefix>-enable`, `<prefix>-disable-line`,
211
282
  * `<prefix>-disable-next-line`) plus whole-file inline rule config
212
- * (bare `<prefix> <rule-config>`), sourced from `Comment` nodes gathered via
213
- * the consumer's `walkShell()` and parsed with
283
+ * (bare `<prefix> <rule-config>`), sourced from the tree's `Comment` nodes
284
+ * and parsed with
214
285
  * `@eslint/plugin-kit`'s `ConfigCommentParser` β€” the same building blocks
215
286
  * `@eslint/json`'s `JSONSourceCode` uses, generalized so the `<prefix>` is
216
287
  * configurable (default `"eslint"`; see `ShellLanguageOptions.directivePrefix`)
@@ -220,6 +291,24 @@ function withUnusedDirectiveFixRange(comment, text) {
220
291
  * classes) is implemented on `ShellLanguage` β€” see `shell-language.ts` and
221
292
  * `selector-classes.ts`.
222
293
  *
294
+ * Rule-author helpers (`resolveWordText`, `getCommandName`,
295
+ * `resolveInvokedCommand`, `getCommandSites`) answer "is this word statically
296
+ * the string `jq`?", "which command does this invocation run?", and "which
297
+ * commands does this file run, and where?" by delegating to
298
+ * `sh-ast/analyze`. They never reinterpret words themselves, so every rule
299
+ * gets the same answer for the same syntax. `dialect` and `shellVersion`
300
+ * expose the file's normalized language options.
301
+ *
302
+ * Syntax access (`getSyntax`, `getViewNode`, `getIndexFromSyntaxPos`,
303
+ * `getLocFromSyntaxPos`) reaches behind the lint view to the sh-ast records
304
+ * this file was parsed into: from a view node to its record and back, and
305
+ * from a record's byte positions (such as `BinaryCmd.OpPos`, which the view
306
+ * leaves out) to ESLint coordinates in this file's text.
307
+ *
308
+ * Printed fixes (`getEditableSyntax`, `getPrintedReplacement`) edit an
309
+ * independent copy of a view node's record and print it with sh-ast's
310
+ * printer, returning the source range and text for `fixer.replaceTextRange`.
311
+ *
223
312
  * @public
224
313
  */
225
314
  export class ShellSourceCode extends TextSourceCodeBase {
@@ -254,7 +343,7 @@ export class ShellSourceCode extends TextSourceCodeBase {
254
343
  */
255
344
  #inlineConfigPattern;
256
345
  /**
257
- * Comments gathered through the consumer's child graph. Source-order sorting
346
+ * Comments in the consumer's child graph. Source-order sorting
258
347
  * is required for repeated inline configuration; schema field order does not
259
348
  * encode physical source order. The lint view is a stable parse snapshot.
260
349
  */
@@ -265,15 +354,460 @@ export class ShellSourceCode extends TextSourceCodeBase {
265
354
  * config, once indirectly via `getDisableDirectives`/`applyInlineConfig`).
266
355
  */
267
356
  #inlineConfigNodes;
357
+ /** The normalized `languageOptions.dialect` this file was parsed with. */
358
+ #dialect;
359
+ /** The configured `languageOptions.shellVersion`, if the config supplied one. */
360
+ #shellVersion;
361
+ /** This file's coordinate index; see `#textCoordinates`. */
362
+ #ownCoordinates;
363
+ /**
364
+ * Every command site in this file, from one `enumerateCommands` call over
365
+ * the original syntax, built the first time a command helper needs it and
366
+ * reused for the rest of the lint run. Files whose rules never ask a
367
+ * command question never pay for (or fail on) enumeration.
368
+ */
369
+ #commandSites;
370
+ /**
371
+ * {@link ShellSourceCode.getCommandSites}'s answer: the index's sites in
372
+ * view terms, built once from `#commandSites` so every caller shares one
373
+ * frozen array.
374
+ */
375
+ #commandSiteViews;
268
376
  constructor(options) {
269
377
  super(options);
270
- // Defensive default for direct construction outside the `ShellLanguage`
378
+ // Defensive defaults for direct construction outside the `ShellLanguage`
271
379
  // pipeline (e.g. unit tests) β€” `ShellLanguage.createSourceCode` always
272
- // supplies the already-normalized, defaulted value.
380
+ // supplies the already-normalized, defaulted values.
273
381
  this.#directivePrefix = options.directivePrefix ?? 'eslint';
382
+ this.#dialect = options.dialect ?? 'bash';
383
+ this.#shellVersion = options.shellVersion;
274
384
  this.#directiveLabels = buildDirectiveLabels(this.#directivePrefix);
275
385
  this.#inlineConfigPattern = buildInlineConfigPattern(this.#directivePrefix);
276
386
  }
387
+ /**
388
+ * The shell dialect this file was parsed as: the normalized
389
+ * `languageOptions.dialect`, which defaults to `"bash"` when the config
390
+ * leaves it unset. Rules use it to gate dialect-specific checks.
391
+ */
392
+ get dialect() {
393
+ return this.#dialect;
394
+ }
395
+ /**
396
+ * The configured `languageOptions.shellVersion` (a `MAJOR[.MINOR]` string
397
+ * such as `"3.2"`), or `undefined` when the config does not set one. The
398
+ * parser never reads it; it exists for rules that gate on shell version.
399
+ */
400
+ get shellVersion() {
401
+ return this.#shellVersion;
402
+ }
403
+ /**
404
+ * The one-based location of a UTF-16 `index` in this file's text, where
405
+ * `index` may be anywhere from 0 to `text.length`. CRLF counts as one line
406
+ * break.
407
+ *
408
+ * Overrides the base class, which measures every index from the root
409
+ * node's own start and so assumes the root spans the whole text. A shell
410
+ * file's root starts at its first statement or comment, so the base answer
411
+ * was shifted by any leading blank lines or indentation.
412
+ *
413
+ * @throws TypeError when `index` is not a number.
414
+ * @throws RangeError when `index` is not an integer from 0 to `text.length`.
415
+ */
416
+ getLocFromIndex(index) {
417
+ if (typeof index !== 'number')
418
+ throw new TypeError('Expected `index` to be a number.');
419
+ const { line, column } = this.#textCoordinates().atIndex(index);
420
+ return { line, column };
421
+ }
422
+ /**
423
+ * The UTF-16 index of a one-based `loc` in this file's text: the inverse of
424
+ * {@link ShellSourceCode.getLocFromIndex}. The column may name any position
425
+ * on its line up to and including the line break.
426
+ *
427
+ * Overrides the base class for the same reason as `getLocFromIndex`.
428
+ *
429
+ * @throws TypeError when `loc` is not an object with numeric `line` and `column`.
430
+ * @throws RangeError when `loc` is not inside the text.
431
+ */
432
+ getIndexFromLoc(loc) {
433
+ if (typeof loc !== 'object' ||
434
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- callers may be untyped JavaScript
435
+ loc === null ||
436
+ typeof loc.line !== 'number' ||
437
+ typeof loc.column !== 'number') {
438
+ throw new TypeError('Expected `loc` to be an object with numeric `line` and `column` properties.');
439
+ }
440
+ return this.#textCoordinates().indexAt(loc.line, loc.column);
441
+ }
442
+ /**
443
+ * The coordinate index of this file's text: the one the language built while
444
+ * parsing, or, for a source code constructed directly, one built from the
445
+ * text on first use.
446
+ */
447
+ #textCoordinates() {
448
+ this.#ownCoordinates ??= projectedCoordinates(this.ast) ?? new SourceCoordinates(this.text);
449
+ return this.#ownCoordinates;
450
+ }
451
+ /**
452
+ * Returns the literal text of `word` when `sh-ast/analyze`'s `resolveWord`
453
+ * reports it static, or `null` otherwise.
454
+ *
455
+ * Delegates to `resolveWord` (through this package's `resolveWord` export),
456
+ * which alone defines what "static" means: a word is static iff
457
+ * `resolveWord` reports `static: true`. Its non-static reasons are
458
+ * `expansion`, `tilde`, `glob`, `brace`, `locale`, and `unsupported`.
459
+ * Call the root `resolveWord` export instead when the reason matters.
460
+ *
461
+ * `options` is forwarded to `resolveWord` unchanged. When it is omitted, the
462
+ * word's grammar position picks the tilde context: a word in a `CallExpr`'s
463
+ * `args` uses `{ context: 'command-argument' }`, the context
464
+ * `enumerateCommands` uses for argv, so `resolveWordText(callExpr.args[0])`
465
+ * equals `getCommandName(callExpr)`. Any other word uses `resolveWord`'s
466
+ * conservative default, which also treats a `~` after an unquoted `:` as a
467
+ * tilde expansion, as assignment values do.
468
+ *
469
+ * @param word - A parsed `Word` node from this file's lint tree.
470
+ * @param options - Forwarded to `resolveWord` unchanged when supplied.
471
+ * @returns The resolved text, or `null` when the word is not static.
472
+ * @throws TypeError when `word` is not a parsed shell `Word`.
473
+ */
474
+ resolveWordText(word, options) {
475
+ const resolution = resolveWord(word, options ?? this.#defaultWordOptions(word));
476
+ return resolution.static ? resolution.text : null;
477
+ }
478
+ /**
479
+ * The `resolveWord` options implied by `word`'s grammar position when the
480
+ * caller supplies none: command-argument context for a `CallExpr` argument,
481
+ * otherwise `resolveWord`'s own conservative default.
482
+ */
483
+ #defaultWordOptions(word) {
484
+ const parent = this.getParent(word);
485
+ const isCommandArgument = parent?.type === 'CallExpr' && Array.isArray(parent.args) && parent.args.includes(word);
486
+ return isCommandArgument ? { context: 'command-argument' } : undefined;
487
+ }
488
+ /**
489
+ * Returns the static text of `callExpr`'s command word (`argv[0]`) as
490
+ * written, or `null` when that word is dynamic or absent. Transparent
491
+ * wrappers are **not** followed: `sudo rm x` yields `"sudo"`. Use
492
+ * {@link ShellSourceCode.resolveInvokedCommand} for the command a wrapper
493
+ * chain actually runs.
494
+ *
495
+ * Delegates to `sh-ast/analyze`: the answer is the `argv0` resolution of the
496
+ * `CommandSite` that `enumerateCommands` reports for this `CallExpr` in this
497
+ * file. Leading `VAR=val` assignments are not part of argv, so
498
+ * `FOO=1 jq .` yields `"jq"`. An assignment-only statement (`FOO=1`) has no
499
+ * argv0 and no `CommandSite`, and yields `null`.
500
+ *
501
+ * @param callExpr - A parsed `CallExpr` node from this file's lint tree.
502
+ * @returns The command word's static text, or `null`.
503
+ * @throws TypeError when `callExpr` is not a parsed shell `CallExpr`.
504
+ * @throws `ShAnalyzeMaxDepthError` (from `sh-ast/analyze`) when this file's
505
+ * nesting exceeds analysis's recursion guard; see
506
+ * {@link ShellSourceCode.resolveInvokedCommand}.
507
+ */
508
+ getCommandName(callExpr) {
509
+ const argv0 = this.#commandSiteFor(callExpr)?.argv0;
510
+ return argv0?.static ? argv0.text : null;
511
+ }
512
+ /**
513
+ * Follows `callExpr`'s argv0 through transparent wrappers (`env`, `sudo`,
514
+ * `nohup`, `exec`, …) and returns `sh-ast/analyze`'s full `resolveArgv0`
515
+ * result, or `null` for a `CallExpr` that has no `CommandSite` (an
516
+ * assignment-only statement such as `FOO=1`).
517
+ *
518
+ * Delegates to `resolveArgv0` with the `CommandSite` that `enumerateCommands`
519
+ * reports for this `CallExpr` in this file, so it inherits that function's
520
+ * guarantee that an unknowable word is never guessed through. Read the
521
+ * effective command's name as
522
+ * `res.effective.static ? res.effective.text : null`.
523
+ * `sudo -u x "$prog"` and `exec "$@"` have a non-static `effective` with
524
+ * reason `expansion`; `env -S '…'` has reason `embedded-command`; `chain`
525
+ * lists every word resolved along the way, outermost wrapper first, and
526
+ * `chainIndices` gives each entry's position in the command's `argv` (as
527
+ * {@link ShellSourceCode.getCommandSites} reports it). When `effective` is
528
+ * static, the effective command's arguments are
529
+ * `argv.slice(chainIndices.at(-1)! + 1)`: every entry after the effective
530
+ * command's own index.
531
+ *
532
+ * Enumeration runs once per file, the first time any command helper is
533
+ * called. If the file's nesting exceeds `enumerateCommands`'s recursion
534
+ * guard, its `ShAnalyzeMaxDepthError` propagates unchanged and ESLint
535
+ * reports the rule as crashed for that file. That fails closed: a partial
536
+ * index would silently omit real commands.
537
+ *
538
+ * @param callExpr - A parsed `CallExpr` node from this file's lint tree.
539
+ * @param options - Forwarded to `resolveArgv0` unchanged.
540
+ * `transparentWrappers` replaces `sh-ast/analyze`'s
541
+ * `DEFAULT_TRANSPARENT_WRAPPERS`; spread the defaults into the array to
542
+ * extend them instead.
543
+ * @returns The resolution, or `null` when `callExpr` invokes no command.
544
+ * @throws TypeError when `callExpr` is not a parsed shell `CallExpr`.
545
+ * @throws `ShAnalyzeMaxDepthError` (from `sh-ast/analyze`) when this file's
546
+ * nesting exceeds analysis's recursion guard.
547
+ * @throws `ShAnalyzeInvalidWrapperSpecError` (from `sh-ast/analyze`) when
548
+ * `options.transparentWrappers` contains a malformed entry.
549
+ */
550
+ resolveInvokedCommand(callExpr, options) {
551
+ const site = this.#commandSiteFor(callExpr);
552
+ return site ? resolveArgv0(site, options) : null;
553
+ }
554
+ /**
555
+ * Returns every command invocation in this file, one entry per
556
+ * `sh-ast/analyze` `CommandSite`, in source order. Use it when a rule needs
557
+ * the whole file's commands with their structural context, such as every
558
+ * command inside a function or inside `$(…)`, instead of visiting
559
+ * `CallExpr` nodes one by one.
560
+ *
561
+ * The data comes from `sh-ast/analyze`'s `enumerateCommands`: facts about
562
+ * how each command is reached and what its words resolve to, never a
563
+ * verdict. Each entry's `context`, `argv0`, and `argv` are analysis's own
564
+ * values, unchanged; `context` lists frames outermost first, as in sh-ast.
565
+ * Each entry's `node` is the `CallExpr` view node (`===` the node ESLint
566
+ * passes to a `CallExpr` visitor), so it can be passed to
567
+ * `context.report()`.
568
+ *
569
+ * An assignment-only statement (`FOO=1`) runs no command and has no entry,
570
+ * although a command substitution in its value (`FOO=$(date)`) does.
571
+ *
572
+ * Enumeration runs at most once per file, shared with
573
+ * {@link ShellSourceCode.getCommandName} and
574
+ * {@link ShellSourceCode.resolveInvokedCommand}, and every call returns the
575
+ * same array. Because every rule shares it, the array and everything in each
576
+ * entry except `node` are deeply frozen (see `ShellCommandSite`): no rule can
577
+ * change what another rule reads. If the file's nesting exceeds `enumerateCommands`'s
578
+ * recursion guard, its `ShAnalyzeMaxDepthError` propagates unchanged, as it
579
+ * does for those helpers: a partial list would silently omit real commands.
580
+ *
581
+ * @returns This file's command sites, in source order.
582
+ * @throws `ShAnalyzeMaxDepthError` (from `sh-ast/analyze`) when this file's
583
+ * nesting exceeds analysis's recursion guard.
584
+ */
585
+ getCommandSites() {
586
+ this.#commandSiteViews ??= viewCommandSites(this.ast, this.#getCommandSiteIndex());
587
+ return this.#commandSiteViews;
588
+ }
589
+ /**
590
+ * Returns the sh-ast record `node` was projected from: this file's original
591
+ * parse of `node`, with every field the lint view leaves out, including
592
+ * named token positions such as `BinaryCmd.OpPos`, `Stmt.Semicolon` and
593
+ * `Redirect.OpPos`. Pass those positions to
594
+ * {@link ShellSourceCode.getIndexFromSyntaxPos} or
595
+ * {@link ShellSourceCode.getLocFromSyntaxPos} to report on them, or pass the
596
+ * record to an `sh-ast/analyze` function.
597
+ *
598
+ * The returned record is read-only by contract. It is the language's
599
+ * original parse, shared by every rule and helper linting this file, and
600
+ * mutating it is unsupported: helpers such as
601
+ * {@link ShellSourceCode.resolveWordText} read the same record and would see
602
+ * the edit. It is not frozen, so a write does not throw. Make edits on an
603
+ * editable copy from {@link ShellSourceCode.getEditableSyntax} instead.
604
+ *
605
+ * @param node - A view node from this file's lint tree.
606
+ * @returns The original sh-ast record, or `undefined` when `node` has none
607
+ * in this file: an auxiliary view (a record whose type is not an sh-ast
608
+ * `Node`, such as the `Slice` of `${x:1:2}`), a view node from another
609
+ * file's tree, or an
610
+ * object that did not come from the parser.
611
+ */
612
+ getSyntax(node) {
613
+ const syntax = syntaxForNode(node);
614
+ // A view node of another file also has a record; it is not this file's.
615
+ return syntax !== undefined && viewForSyntax(this.ast, syntax) === node ? syntax : undefined;
616
+ }
617
+ /**
618
+ * Returns the lint view node projected from `syntax`: the inverse of
619
+ * {@link ShellSourceCode.getSyntax}. The result is the same object (`===`)
620
+ * ESLint passes to rule visitors, so it can be passed to
621
+ * `context.report()`. Use it to come back to the view after navigating the
622
+ * original records, for example from a `Stmt` record's `Cmd`.
623
+ *
624
+ * Lookup is by object identity, so only the parser's own records match.
625
+ * sh-ast's `walk` passes its callback a fresh copy of each comment stored
626
+ * on a `Stmt`, `CaseItem` or `ArrayElem`, as Go's `syntax.Walk` does, so
627
+ * those `Comment` copies have no view node. Read the stored comment from
628
+ * the record's `Comments` array instead.
629
+ *
630
+ * @param syntax - An sh-ast `Node` record from this file's original parse.
631
+ * @returns The view node, or `undefined` for any other record, including a
632
+ * copy of one of this file's records (such as a `structuredClone`) and a
633
+ * record from another parse.
634
+ */
635
+ getViewNode(syntax) {
636
+ return viewForSyntax(this.ast, syntax);
637
+ }
638
+ /**
639
+ * Returns an independent, editable deep copy of the sh-ast record `node`
640
+ * was parsed from: the first half of a printed fix. Edit the copy, then pass
641
+ * it to {@link ShellSourceCode.getPrintedReplacement}.
642
+ *
643
+ * The copy is `structuredClone` of the record
644
+ * {@link ShellSourceCode.getSyntax} returns. sh-ast documents and tests
645
+ * ordinary `structuredClone` as the supported way to make an independent
646
+ * editable copy of a syntax subtree (sh-ast 0.7.0 README, "Editing and
647
+ * printing"), so no second parse is involved. The copy owns its records,
648
+ * arrays, positions and byte buffers. Editing it leaves this file's original
649
+ * syntax, its lint view, its cached traversal and the answers of view-aware
650
+ * helpers such as `resolveWord` and
651
+ * {@link ShellSourceCode.resolveWordText} unchanged. Each call returns a new
652
+ * copy.
653
+ *
654
+ * The copy's position fields keep their original values, and the copy has
655
+ * no view node: {@link ShellSourceCode.getViewNode},
656
+ * {@link ShellSourceCode.getIndexFromSyntaxPos} and the parse's position
657
+ * snapshot describe the original source, not the edit. Keep `node` to know
658
+ * which source range the fix replaces.
659
+ *
660
+ * @param node - A view node from this file's lint tree.
661
+ * @returns A deep copy of `node`'s original sh-ast record.
662
+ * @throws TypeError when `node` has no original record in this file: an
663
+ * auxiliary view (such as the `Slice` of `${x:1:2}`), a view node of another
664
+ * file, or an object that did not come from the parser.
665
+ */
666
+ getEditableSyntax(node) {
667
+ return structuredClone(this.#originalSyntaxFor(node));
668
+ }
669
+ /**
670
+ * Prints `edited` with sh-ast's `printSync` and pairs the text with the
671
+ * source range it replaces, so a printed fix is
672
+ * `fix: (fixer) => fixer.replaceTextRange(r.range, r.text)`. `edited` is
673
+ * usually a copy from {@link ShellSourceCode.getEditableSyntax} of `node`
674
+ * or of one of its ancestors.
675
+ *
676
+ * The range is `node.range`, except that a `File` root replaces the whole
677
+ * text (`[0, text.length]`) whatever `node` is, which allows whole-file
678
+ * regeneration. `options` is passed to `printSync` unchanged.
679
+ *
680
+ * The printer formats; it does not replay the source. Its output starts at
681
+ * column 0 and indents nested lines with upstream's indentation (tabs by
682
+ * default, `Indent` spaces when `options.Indent` is set). Replacing a
683
+ * multi-line root nested inside other code, such as an `if` statement in a
684
+ * function body, therefore keeps the shell meaning but may change the
685
+ * formatting of its lines. Use a `Word` or `WordPart` root for a surgical
686
+ * fix that leaves the surrounding source as written.
687
+ *
688
+ * A `Stmt` root is adjusted so its text matches `node.range`. A statement's
689
+ * own leading and trailing comments lie outside its range, so they are not
690
+ * printed (the copy passed in is not modified). When `node` is a statement
691
+ * whose range ends in its `;` or `&` terminator, upstream prints the
692
+ * statement without a `;`, so a `;` is appended unless the edited statement
693
+ * prints its own `&`, `|&` or disown operator. Without this, replacing `a`
694
+ * in `a; b` would produce `a b`.
695
+ *
696
+ * @param node - A view node from this file's lint tree: the node the fix
697
+ * replaces.
698
+ * @param edited - The record to print: a `File`, `Stmt`, `Word`, `Assign`,
699
+ * `Command` or `WordPart`.
700
+ * @param options - Forwarded to `printSync` unchanged.
701
+ * @returns The range to replace and the printed text to put there.
702
+ * @throws TypeError when `node` has no original record in this file (see
703
+ * {@link ShellSourceCode.getEditableSyntax}), when `edited` is not a
704
+ * printable root (the message names its type), or when the printer's output
705
+ * is not valid UTF-8. Non-UTF-8 output comes back from `printSync` as a
706
+ * `Uint8Array` and is never decoded lossily into a fix.
707
+ * @throws `PrintError` or `PrintPanicError` (from sh-ast) when upstream's
708
+ * printer fails for `edited` or `options`.
709
+ */
710
+ getPrintedReplacement(node, edited, options) {
711
+ const original = this.#originalSyntaxFor(node);
712
+ const rootType = printableRootType(edited);
713
+ const root = edited.Type === 'Stmt' ? { ...edited, Comments: null } : edited;
714
+ const printed = printSync(root, options);
715
+ if (typeof printed !== 'string') {
716
+ throw new TypeError('printSync produced output that is not valid UTF-8; a text fix cannot hold it without ' +
717
+ 'lossy decoding');
718
+ }
719
+ if (rootType === 'File') {
720
+ return { range: [0, this.text.length], text: printed };
721
+ }
722
+ const dropsTerminator = edited.Type === 'Stmt' &&
723
+ original.Type === 'Stmt' &&
724
+ original.Semicolon.IsValid &&
725
+ !printsOwnTerminator(edited);
726
+ return {
727
+ range: [node.range[0], node.range[1]],
728
+ text: dropsTerminator ? `${printed};` : printed,
729
+ };
730
+ }
731
+ /**
732
+ * `node`'s original record in this file, or a `TypeError` when it has none,
733
+ * so the printed-fix helpers never copy or replace a range they cannot tie
734
+ * to this file's parse.
735
+ */
736
+ #originalSyntaxFor(node) {
737
+ const syntax = this.getSyntax(node);
738
+ if (syntax === undefined) {
739
+ throw new TypeError('expected a view node parsed from this file; auxiliary views, view nodes of other ' +
740
+ 'files and objects that did not come from the parser have no original syntax');
741
+ }
742
+ return syntax;
743
+ }
744
+ /**
745
+ * Converts a byte position from this file's original parse into the
746
+ * UTF-16 index ESLint uses for `range` and `text.slice`. sh-ast positions
747
+ * count UTF-8 bytes, so a raw `Offset` is wrong for any source with
748
+ * non-ASCII text before it. For example, in `echo πŸŽ‰ && echo δΈ­ζ–‡`,
749
+ * `getIndexFromSyntaxPos(binaryCmd.OpPos)` is the index where `&&` starts.
750
+ *
751
+ * The conversion is only correct for positions of this file's original
752
+ * parse, as read from {@link ShellSourceCode.getSyntax}'s records. The live
753
+ * positions of an edited or copied tree describe that edit, not this
754
+ * source.
755
+ *
756
+ * @param pos - A `SyntaxPos` from one of this file's original records.
757
+ * @returns The UTF-16 index of `pos` in this file's text.
758
+ * @throws RangeError when `pos` is invalid (`IsValid === false`), recovered
759
+ * (`IsRecovered === true`), or overflowed (`OffsetOverflow === true`), or
760
+ * when its offset is not a UTF-8 character boundary of this source. Such a
761
+ * position is never rounded to a nearby boundary.
762
+ * @throws TypeError when `pos` is not a `SyntaxPos`, or when this source
763
+ * code's tree was not built by this language's parser.
764
+ */
765
+ getIndexFromSyntaxPos(pos) {
766
+ return this.#coordinateAt(pos).index;
767
+ }
768
+ /**
769
+ * Converts a byte position from this file's original parse into a one-based
770
+ * ESLint location, the shape `context.report({ loc })` takes. The column
771
+ * counts UTF-16 code units, not bytes, and CRLF counts as one line break.
772
+ * Same contract as {@link ShellSourceCode.getIndexFromSyntaxPos}.
773
+ *
774
+ * @param pos - A `SyntaxPos` from one of this file's original records.
775
+ * @returns The one-based line and UTF-16 column of `pos`.
776
+ * @throws RangeError when `pos` is invalid, recovered, or overflowed, or
777
+ * when its offset is not a UTF-8 character boundary of this source.
778
+ * @throws TypeError when `pos` is not a `SyntaxPos`, or when this source
779
+ * code's tree was not built by this language's parser.
780
+ */
781
+ getLocFromSyntaxPos(pos) {
782
+ const { line, column } = this.#coordinateAt(pos);
783
+ return { line, column };
784
+ }
785
+ /**
786
+ * Converts `pos` with the coordinate index projection built for this
787
+ * file's source, so the file pays for one index however many positions
788
+ * rules convert.
789
+ */
790
+ #coordinateAt(pos) {
791
+ const coordinates = projectedCoordinates(this.ast);
792
+ if (coordinates === undefined) {
793
+ throw new TypeError('expected a parsed shell File from this language adapter');
794
+ }
795
+ return coordinates.fromByteOffset(exactByteOffset(pos));
796
+ }
797
+ /** This file's command-site index, built by one `enumerateCommands` call on first use. */
798
+ #getCommandSiteIndex() {
799
+ this.#commandSites ??= buildCommandSiteIndex(this.ast);
800
+ return this.#commandSites;
801
+ }
802
+ /**
803
+ * Looks up `callExpr`'s `CommandSite` in this file's lazily built index.
804
+ * The `CallExpr` is validated before the index is built, so a misuse
805
+ * reports `TypeError` rather than an unrelated analysis error.
806
+ */
807
+ #commandSiteFor(callExpr) {
808
+ const raw = rawCallExprFor(callExpr);
809
+ return this.#getCommandSiteIndex().byNode.get(raw);
810
+ }
277
811
  /**
278
812
  * Returns the parent of `node`, or `undefined` for the root `File` node
279
813
  * (or any node visited before `traverse()` has run once). ESLint always
@@ -300,19 +834,8 @@ export class ShellSourceCode extends TextSourceCodeBase {
300
834
  this.#parents.set(node, parent);
301
835
  }
302
836
  steps.push(new VisitNodeStep({ target: node, phase: 1, args: [node, parent] }));
303
- const keys = shellVisitorKeys[node.type] ?? [];
304
- for (const key of keys) {
305
- const value = node[key];
306
- if (Array.isArray(value)) {
307
- for (const child of value) {
308
- if (isShellNode(child)) {
309
- visit(child, node);
310
- }
311
- }
312
- }
313
- else if (isShellNode(value)) {
314
- visit(value, node);
315
- }
837
+ for (const child of childNodes(node)) {
838
+ visit(child, node);
316
839
  }
317
840
  steps.push(new VisitNodeStep({ target: node, phase: 2, args: [node, parent] }));
318
841
  };
@@ -323,15 +846,22 @@ export class ShellSourceCode extends TextSourceCodeBase {
323
846
  * Returns retained comments in source order. Reflection-field order can place
324
847
  * a later enclosing comment before an earlier nested comment, so sort by the
325
848
  * exact derived source span before directive and inline-config processing.
849
+ *
850
+ * A tree built by the parser already recorded its comments during projection,
851
+ * so no extra tree walk is needed. Only a tree constructed some other way
852
+ * (for example, passed directly to the constructor) is walked to find them.
326
853
  */
327
854
  #getComments() {
328
855
  if (!this.#comments) {
329
- const comments = [];
330
- walkShell(this.ast, (node) => {
331
- if (isShellComment(node)) {
332
- comments.push(node);
333
- }
334
- });
856
+ const projected = projectedComments(this.ast);
857
+ const comments = projected === undefined ? [] : [...projected];
858
+ if (projected === undefined) {
859
+ walkShell(this.ast, (node) => {
860
+ if (isShellComment(node)) {
861
+ comments.push(node);
862
+ }
863
+ });
864
+ }
335
865
  comments.sort((a, b) => a.range[0] - b.range[0] || a.range[1] - b.range[1]);
336
866
  this.#comments = comments;
337
867
  }
@@ -418,4 +948,3 @@ export class ShellSourceCode extends TextSourceCodeBase {
418
948
  return { configs, problems };
419
949
  }
420
950
  }
421
- //# sourceMappingURL=shell-source-code.js.map