@eslint-sh/eslint-plugin 0.6.0 → 0.8.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 (147) hide show
  1. package/README.md +19 -8
  2. package/dist/command-harvest/builtins/bash.d.ts +1 -1
  3. package/dist/command-harvest/builtins/bash.js +1 -1
  4. package/dist/command-harvest/builtins/bats.d.ts +2 -0
  5. package/dist/command-harvest/builtins/bats.js +3 -0
  6. package/dist/command-harvest/wrapper-table.d.ts +3 -19
  7. package/dist/command-harvest/wrapper-table.js +8 -38
  8. package/dist/configs/bash-3-2.d.ts +7 -6
  9. package/dist/configs/bash-3-2.js +5 -3
  10. package/dist/configs/posix-portable.d.ts +1 -1
  11. package/dist/configs/posix-portable.js +1 -0
  12. package/dist/declaration-conventions/read-declared-commands.d.ts +2 -2
  13. package/dist/declaration-conventions/read-declared-commands.js +2 -2
  14. package/dist/eslint-plugin-syntax.d.ts +4 -0
  15. package/dist/eslint-plugin.d.ts +86 -40
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.js +4 -2
  18. package/dist/language/shell-language-options.d.ts +18 -15
  19. package/dist/language/shell-language-options.js +33 -26
  20. package/dist/language/shell-language.d.ts +10 -9
  21. package/dist/language/shell-language.js +18 -35
  22. package/dist/language/shell-source-code.d.ts +17 -9
  23. package/dist/language/shell-source-code.js +53 -12
  24. package/dist/rules/bash-variable-semantics.d.ts +22 -0
  25. package/dist/rules/bash-variable-semantics.js +41 -0
  26. package/dist/rules/compat/create-compat-rule.js +1 -1
  27. package/dist/rules/compat/declaration-options.d.ts +2 -2
  28. package/dist/rules/compat/declaration-options.js +5 -2
  29. package/dist/rules/compat/no-append-all-redirect.js +1 -1
  30. package/dist/rules/compat/no-associative-arrays.js +2 -2
  31. package/dist/rules/compat/no-brace-sequence-padding-step.d.ts +15 -0
  32. package/dist/rules/compat/no-brace-sequence-padding-step.js +287 -0
  33. package/dist/rules/compat/no-case-attributes.js +3 -3
  34. package/dist/rules/compat/no-case-fallthrough.js +2 -2
  35. package/dist/rules/compat/no-case-modification.js +2 -2
  36. package/dist/rules/compat/no-coproc.js +1 -1
  37. package/dist/rules/compat/no-declare-g.js +3 -3
  38. package/dist/rules/compat/no-double-bracket.js +2 -2
  39. package/dist/rules/compat/no-epoch-variables.js +2 -2
  40. package/dist/rules/compat/no-fd-variable.js +1 -1
  41. package/dist/rules/compat/no-globstar.js +1 -1
  42. package/dist/rules/compat/no-lastpipe-semantics.js +2 -2
  43. package/dist/rules/compat/no-let.js +2 -1
  44. package/dist/rules/compat/no-mapfile.js +4 -2
  45. package/dist/rules/compat/no-nameref.js +3 -3
  46. package/dist/rules/compat/no-negative-array-index.js +39 -9
  47. package/dist/rules/compat/no-nounset-empty-array.js +15 -4
  48. package/dist/rules/compat/no-parameter-transformation.js +2 -2
  49. package/dist/rules/compat/no-pipe-all.js +1 -1
  50. package/dist/rules/compat/no-printf-time-format.js +4 -2
  51. package/dist/rules/compat/no-source.js +2 -1
  52. package/dist/rules/compat/no-test-v.js +4 -2
  53. package/dist/rules/compat/no-unicode-escape.js +2 -2
  54. package/dist/rules/compat/no-wait-n.js +4 -2
  55. package/dist/rules/compat/nounset-array-proof.d.ts +8 -0
  56. package/dist/rules/compat/nounset-array-proof.js +563 -0
  57. package/dist/rules/compat/posix-absent-command.d.ts +7 -8
  58. package/dist/rules/compat/posix-absent-command.js +17 -15
  59. package/dist/rules/compat/shopt-options.js +3 -1
  60. package/dist/rules/compat/version-gates.d.ts +10 -10
  61. package/dist/rules/compat/version-gates.js +21 -13
  62. package/dist/rules/create-wrapper-rule.js +12 -7
  63. package/dist/rules/index.js +2 -0
  64. package/dist/rules/no-dynamic-invocation.d.ts +0 -8
  65. package/dist/rules/no-dynamic-invocation.js +7 -15
  66. package/dist/rules/no-eval.js +3 -1
  67. package/dist/rules/no-masked-exit-status.js +15 -8
  68. package/dist/rules/no-secret-env-reads.d.ts +4 -3
  69. package/dist/rules/no-secret-env-reads.js +37 -14
  70. package/dist/rules/no-secret-flags.d.ts +2 -1
  71. package/dist/rules/no-secret-flags.js +7 -8
  72. package/dist/rules/no-unchecked-cd.js +3 -1
  73. package/dist/rules/no-undeclared-commands.d.ts +1 -15
  74. package/dist/rules/no-undeclared-commands.js +3 -15
  75. package/dist/rules/no-unused-declarations.d.ts +1 -18
  76. package/dist/rules/no-unused-declarations.js +9 -34
  77. package/dist/rules/prefer-local.js +4 -3
  78. package/dist/rules/quote-expansions.js +3 -28
  79. package/dist/rules/require-network-wrapper.d.ts +2 -17
  80. package/dist/rules/require-network-wrapper.js +33 -4
  81. package/dist/rules/require-tty-guard.js +30 -1
  82. package/dist/rules/resolve-declaration-convention.d.ts +5 -6
  83. package/dist/rules/resolve-declaration-convention.js +5 -6
  84. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  85. package/dist/rules/resolve-network-wrapper.js +57 -26
  86. package/dist/rules/resolve-transparent-wrappers.d.ts +43 -0
  87. package/dist/rules/resolve-transparent-wrappers.js +98 -0
  88. package/dist/rules/set-options.d.ts +2 -1
  89. package/dist/rules/set-options.js +17 -5
  90. package/dist/rules/shebang-interpreter.js +4 -2
  91. package/dist/rules/transparent-wrappers.d.ts +4 -3
  92. package/dist/rules/transparent-wrappers.js +5 -4
  93. package/dist/rules/user-pattern.d.ts +5 -3
  94. package/dist/rules/user-pattern.js +6 -4
  95. package/dist/syntax/index.d.ts +4 -0
  96. package/dist/syntax/index.js +4 -0
  97. package/docs/compat.md +90 -34
  98. package/docs/custom-rule-tutorial.md +9 -3
  99. package/docs/examples/no-listed-commands-rule.ts +2 -2
  100. package/docs/rules/README.md +38 -5
  101. package/docs/rules/_template.md +2 -2
  102. package/docs/rules/compat/no-append-all-redirect.md +12 -12
  103. package/docs/rules/compat/no-associative-arrays.md +14 -14
  104. package/docs/rules/compat/no-brace-sequence-padding-step.md +90 -0
  105. package/docs/rules/compat/no-case-attributes.md +13 -13
  106. package/docs/rules/compat/no-case-fallthrough.md +12 -12
  107. package/docs/rules/compat/no-case-modification.md +11 -11
  108. package/docs/rules/compat/no-coproc.md +13 -13
  109. package/docs/rules/compat/no-declare-g.md +13 -13
  110. package/docs/rules/compat/no-double-bracket.md +2 -2
  111. package/docs/rules/compat/no-epoch-variables.md +13 -13
  112. package/docs/rules/compat/no-fd-variable.md +12 -12
  113. package/docs/rules/compat/no-globstar.md +13 -13
  114. package/docs/rules/compat/no-lastpipe-semantics.md +13 -13
  115. package/docs/rules/compat/no-let.md +2 -2
  116. package/docs/rules/compat/no-mapfile.md +16 -16
  117. package/docs/rules/compat/no-nameref.md +13 -13
  118. package/docs/rules/compat/no-negative-array-index.md +14 -14
  119. package/docs/rules/compat/no-nounset-empty-array.md +72 -17
  120. package/docs/rules/compat/no-parameter-transformation.md +19 -19
  121. package/docs/rules/compat/no-pipe-all.md +13 -13
  122. package/docs/rules/compat/no-printf-time-format.md +13 -13
  123. package/docs/rules/compat/no-source.md +2 -2
  124. package/docs/rules/compat/no-test-v.md +12 -12
  125. package/docs/rules/compat/no-unicode-escape.md +13 -13
  126. package/docs/rules/compat/no-wait-n.md +13 -13
  127. package/docs/rules/no-dynamic-invocation.md +17 -12
  128. package/docs/rules/no-eval.md +6 -1
  129. package/docs/rules/no-masked-exit-status.md +4 -0
  130. package/docs/rules/no-secret-env-reads.md +34 -3
  131. package/docs/rules/no-secret-flags.md +19 -2
  132. package/docs/rules/no-unchecked-cd.md +9 -2
  133. package/docs/rules/no-undeclared-commands.md +16 -15
  134. package/docs/rules/no-unused-declarations.md +17 -20
  135. package/docs/rules/prefer-local.md +20 -2
  136. package/docs/rules/quote-expansions.md +8 -5
  137. package/docs/rules/require-network-wrapper.md +22 -17
  138. package/docs/rules/require-shebang.md +10 -4
  139. package/docs/rules/require-strict-mode.md +6 -0
  140. package/docs/rules/require-tty-guard.md +4 -1
  141. package/docs/shellcheck.md +10 -5
  142. package/docs/transparent-wrappers.md +97 -0
  143. package/docs/untrusted-code.md +440 -0
  144. package/docs/wrapper-rules.md +9 -0
  145. package/package.json +2 -2
  146. package/dist/language/end-of-input-marker.d.ts +0 -29
  147. package/dist/language/end-of-input-marker.js +0 -88
@@ -15,12 +15,13 @@
15
15
  * so the two agree only for arrays without holes; the suggestion message says
16
16
  * so. Assignments and other expansion forms are reported without a suggestion.
17
17
  *
18
- * What is read: an `Index` that is a unary minus applied to a static decimal
19
- * integer, on a `ParamExp` or an `Assign`. A computed subscript such as
18
+ * What is read: an `Index` that is a unary minus applied to a static
19
+ * integer (decimal, octal, hexadecimal or base#digits), on a `ParamExp` or an `Assign`.
20
+ * A computed subscript such as
20
21
  * `-$n` is not a literal and is not reported, and neither is `-0`, which is
21
22
  * zero. A `ParamExp` or `Assign` subscript is read from the syntax tree, so
22
23
  * its operand can be any arithmetic node (`-(1)` holds a parenthesized one);
23
- * only a plain word is resolved.
24
+ * only a plain word is resolved. Arithmetic expressions are never evaluated.
24
25
  *
25
26
  * Unsetting an element (`unset arr[-1]`, quoted or not) is reported from the
26
27
  * `unset` command's operand text, since the parser keeps that operand as an
@@ -28,6 +29,7 @@
28
29
  */
29
30
  import { Minus } from 'sh-ast';
30
31
  import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
32
+ import { resolveTransparentWrappers } from '../resolve-transparent-wrappers.js';
31
33
  import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
32
34
  import { createCompatRule } from './create-compat-rule.js';
33
35
  /** A bare negative-subscript read, the only shape the suggestion rewrites. */
@@ -41,7 +43,33 @@ const TRANSPARENT_WRAPPERS = [
41
43
  BUILTIN_WRAPPER,
42
44
  ];
43
45
  /** An `unset` operand naming an element by a literal negative subscript. */
44
- const UNSET_NEGATIVE = /^[A-Za-z_][A-Za-z0-9_]*\[\s*-\s*\d+\s*\]$/u;
46
+ const UNSET_NEGATIVE = /^[A-Za-z_][A-Za-z0-9_]*\[\s*-\s*([^\s\]]+)\s*\]$/u;
47
+ /**
48
+ * Recognizes a nonzero integer token using Bash's digit alphabets, without evaluating expressions.
49
+ * Leading zero means octal; base#digits requires an unpadded base from 2..64. Variables and invalid digits
50
+ * provide no static evidence. Machine-integer overflow is outside this lexical recognizer.
51
+ */
52
+ function isNonzeroInteger(text) {
53
+ if (/^0[xX][0-9a-fA-F]+$/u.test(text))
54
+ return /[1-9a-fA-F]/u.test(text.slice(2));
55
+ if (/^0[0-7]*$/u.test(text))
56
+ return /[1-7]/u.test(text);
57
+ if (/^[1-9][0-9]*$/u.test(text))
58
+ return true;
59
+ const based = /^([1-9]\d?)#([0-9a-zA-Z@_]+)$/u.exec(text);
60
+ if (based === null)
61
+ return false;
62
+ const base = Number(based[1]);
63
+ if (base < 2 || base > 64)
64
+ return false;
65
+ const digits = base <= 36 ? based[2].toLowerCase() : based[2];
66
+ const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ@_'.slice(0, base);
67
+ for (const digit of digits) {
68
+ if (!alphabet.includes(digit))
69
+ return false;
70
+ }
71
+ return /[^0]/u.test(digits);
72
+ }
45
73
  /**
46
74
  * The `sh/compat/no-negative-array-index` rule module.
47
75
  *
@@ -57,13 +85,14 @@ export const noNegativeArrayIndex = createCompatRule({
57
85
  description: 'Disallow negative array subscripts (`${arr[-1]}`) when the target shell lacks them',
58
86
  },
59
87
  messages: {
60
- negativeIndex: 'Negative array subscripts need {{required}}, but this file targets {{target}}. Count from the length instead, for example `${arr[$((${#arr[@]}-1))]}`.',
88
+ negativeIndex: 'Negative array subscripts need {{required}}, but this file must support {{target}}. Count from the length instead, for example `${arr[$((${#arr[@]}-1))]}`.',
61
89
  useArrayLength: 'Replace with `${{{name}}[$((${#{{name}}[@]}-{{n}}))]}` (the two differ only for an array with unset elements).',
62
90
  },
63
91
  },
64
92
  create(context, gate) {
65
93
  const { sourceCode } = context;
66
- /** Whether `index` is a unary minus on a static decimal integer. */
94
+ const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
95
+ /** Whether `index` is a unary minus on a nonzero static integer token. */
67
96
  function isNegativeLiteral(index) {
68
97
  if (index?.type !== 'UnaryArithm' || index.op !== Minus)
69
98
  return false;
@@ -73,12 +102,12 @@ export const noNegativeArrayIndex = createCompatRule({
73
102
  if (operand?.type !== 'Word')
74
103
  return false;
75
104
  const digits = sourceCode.resolveWordText(operand) ?? '';
76
- return /^\d+$/u.test(digits) && /[1-9]/u.test(digits);
105
+ return isNonzeroInteger(digits);
77
106
  }
78
107
  return {
79
108
  CallExpr(node) {
80
109
  const invoked = sourceCode.resolveInvokedCommand(node, {
81
- transparentWrappers: TRANSPARENT_WRAPPERS,
110
+ transparentWrappers,
82
111
  });
83
112
  if (invoked?.effective.static !== true || invoked.effective.text !== 'unset')
84
113
  return;
@@ -89,7 +118,8 @@ export const noNegativeArrayIndex = createCompatRule({
89
118
  const text = sourceCode.resolveWordText(word) ?? sourceCode.getText(word);
90
119
  if (text === '--')
91
120
  continue;
92
- if (UNSET_NEGATIVE.test(text) && /[1-9]/u.test(text)) {
121
+ const match = UNSET_NEGATIVE.exec(text);
122
+ if (match !== null && isNonzeroInteger(match[1])) {
93
123
  context.report({ node: word, messageId: 'negativeIndex', data: { ...gate } });
94
124
  }
95
125
  }
@@ -24,8 +24,8 @@
24
24
  * - whether the file turns `nounset` on anywhere: a `set` command that
25
25
  * enables it (`set -u`, `set -eu`, `set -o nounset`), or a shebang that
26
26
  * passes the flag (`#!/bin/bash -eu`). Where in the file that happens is
27
- * not considered, since a function defined before `set -u` can run after
28
- * it;
27
+ * not itself a proof of execution order: a function defined before
28
+ * `set -u` can run after it;
29
29
  * - every `${name[@]}` and `${name[*]}` with no operator, except inside
30
30
  * `${name[@]+…}` or `${name[@]:+…}` of the same array (the guard this rule
31
31
  * suggests), whose word Bash expands only when the array is set. `${#a[@]}`,
@@ -34,9 +34,17 @@
34
34
  * replacement `${a[@]/x/y}` were observed to work on Bash 3.2 to 4.1 and
35
35
  * fail only on 4.2 and 4.3, which a minimum-version gate cannot express, so
36
36
  * they are not reported either.
37
+ *
38
+ * Local safety proofs suppress candidates when a preceding static `set +u`,
39
+ * literal non-empty array assignment, or length guard establishes safety in
40
+ * the same statement list. Calls and compound statements invalidate facts;
41
+ * dynamic environment changes and variable attributes limit these proofs.
42
+ * `nounset-array-proof.ts` owns those boundaries. Reports remain conditional:
43
+ * unknown state is a reason to report, not proof that the array is empty.
37
44
  */
38
45
  import { AlternateUnset, AlternateUnsetOrNull } from 'sh-ast';
39
46
  import { createCompatRule } from './create-compat-rule.js';
47
+ import { createArraySafetyProof } from './nounset-array-proof.js';
40
48
  import { parseSetArguments, shebangOptionChanges } from '../set-options.js';
41
49
  /** A shell variable name. */
42
50
  const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/u;
@@ -68,7 +76,7 @@ export const noNounsetEmptyArray = createCompatRule({
68
76
  description: 'Disallow whole-array expansions under `set -u` when the target shell aborts on an empty array',
69
77
  },
70
78
  messages: {
71
- emptyArray: 'If this array is empty, `${{{name}}[{{subscript}}]}` under `set -u` needs {{required}}, but this file targets {{target}}, where it aborts with "unbound variable". Guard it, for example `{{example}}`.',
79
+ emptyArray: 'This expansion is not proven safe. If this array is empty, `${{{name}}[{{subscript}}]}` under `set -u` needs {{required}}, but this file must support {{target}}, where it aborts with "unbound variable". Guard it, for example `{{example}}`.',
72
80
  guardExpansion: 'Replace with `{{replacement}}`.',
73
81
  },
74
82
  },
@@ -132,9 +140,12 @@ export const noNounsetEmptyArray = createCompatRule({
132
140
  candidates.push({ node, name, subscript });
133
141
  },
134
142
  'File:exit'() {
135
- if (!nounset)
143
+ if (!nounset || candidates.length === 0)
136
144
  return;
145
+ const provenSafe = createArraySafetyProof(sourceCode);
137
146
  for (const { node, name, subscript } of candidates) {
147
+ if (provenSafe(node, name))
148
+ continue;
138
149
  const replacement = guardedExpansion(name, subscript);
139
150
  const quoted = wholeQuotedWord(node);
140
151
  context.report({
@@ -56,8 +56,8 @@ export const noParameterTransformation = createMultiFeatureCompatRule({
56
56
  description: 'Disallow parameter transformations (`${v@Q}`, `${v@U}` and the others) when the target shell lacks the operator',
57
57
  },
58
58
  messages: {
59
- transformation: '`${…@{{operator}}}` needs {{required}}, but this file targets {{target}}. Use {{alternative}} instead.',
60
- transformationNoAlternative: '`${…@{{operator}}}` needs {{required}}, but this file targets {{target}}, which has no equivalent expansion; restructure the code that needs it.',
59
+ transformation: '`${…@{{operator}}}` needs {{required}}, but this file must support {{target}}. Use {{alternative}} instead.',
60
+ transformationNoAlternative: '`${…@{{operator}}}` needs {{required}}, but this file must support {{target}}, which has no equivalent expansion; restructure the code that needs it.',
61
61
  },
62
62
  },
63
63
  create(context, gates) {
@@ -31,7 +31,7 @@ export const noPipeAll = createCompatRule({
31
31
  description: 'Disallow the `|&` pipe when the target shell lacks it',
32
32
  },
33
33
  messages: {
34
- pipeAll: '`|&` needs {{required}}, but this file targets {{target}}. Write `cmd 2>&1 | other` instead.',
34
+ pipeAll: '`|&` needs {{required}}, but this file must support {{target}}. Write `cmd 2>&1 | other` instead.',
35
35
  useStderrRedirect: 'Replace `|&` with `2>&1 |`.',
36
36
  },
37
37
  },
@@ -16,6 +16,7 @@
16
16
  * word.
17
17
  */
18
18
  import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
19
+ import { resolveTransparentWrappers } from '../resolve-transparent-wrappers.js';
19
20
  import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
20
21
  import { createCompatRule } from './create-compat-rule.js';
21
22
  /**
@@ -65,15 +66,16 @@ export const noPrintfTimeFormat = createCompatRule({
65
66
  description: "Disallow `printf`'s `%(fmt)T` date format when the target shell lacks it",
66
67
  },
67
68
  messages: {
68
- printfTime: "`printf '%({{format}})T'` needs {{required}}, but this file targets {{target}}. For the current time, run `date '+{{format}}'` instead.",
69
+ printfTime: "`printf '%({{format}})T'` needs {{required}}, but this file must support {{target}}. For the current time, run `date '+{{format}}'` instead.",
69
70
  },
70
71
  },
71
72
  create(context, gate) {
72
73
  const { sourceCode } = context;
74
+ const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
73
75
  return {
74
76
  CallExpr(node) {
75
77
  const invoked = sourceCode.resolveInvokedCommand(node, {
76
- transparentWrappers: TRANSPARENT_WRAPPERS,
78
+ transparentWrappers,
77
79
  });
78
80
  if (invoked?.effective.static !== true || invoked.effective.text !== 'printf')
79
81
  return;
@@ -81,9 +81,10 @@ export const noSource = createCompatRule({
81
81
  create(context, gate) {
82
82
  const { sourceCode } = context;
83
83
  const fileDefinesSource = definedFunctionNames(sourceCode).has(SOURCE);
84
+ const reachableWord = reachableCommandWord(context, SOURCE);
84
85
  return {
85
86
  CallExpr(node) {
86
- const word = reachableCommandWord(sourceCode, node, SOURCE);
87
+ const word = reachableWord(node);
87
88
  if (word === undefined)
88
89
  return;
89
90
  // Only a name written directly can run a function: a wrapper skips it.
@@ -27,6 +27,7 @@
27
27
  */
28
28
  import { TsVarSet } from 'sh-ast';
29
29
  import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
30
+ import { resolveTransparentWrappers } from '../resolve-transparent-wrappers.js';
30
31
  import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
31
32
  import { createCompatRule } from './create-compat-rule.js';
32
33
  /**
@@ -214,11 +215,12 @@ export const noTestV = createCompatRule({
214
215
  description: 'Disallow the `-v name` test (`[[ -v x ]]`, `test -v x`) when the target shell lacks it',
215
216
  },
216
217
  messages: {
217
- testV: '`-v {{name}}` needs {{required}}, but this file targets {{target}}. Test whether the variable is set with `[ -n "${{{name}}+set}" ]` instead.',
218
+ testV: '`-v {{name}}` needs {{required}}, but this file must support {{target}}. Test whether the variable is set with `[ -n "${{{name}}+set}" ]` instead.',
218
219
  },
219
220
  },
220
221
  create(context, gate) {
221
222
  const { sourceCode } = context;
223
+ const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
222
224
  /** The variable name shown in the message for `operand`. */
223
225
  function nameOf(operand) {
224
226
  const text = operand?.type === 'Word' ? sourceCode.resolveWordText(operand) : null;
@@ -251,7 +253,7 @@ export const noTestV = createCompatRule({
251
253
  },
252
254
  CallExpr(node) {
253
255
  const invoked = sourceCode.resolveInvokedCommand(node, {
254
- transparentWrappers: TRANSPARENT_WRAPPERS,
256
+ transparentWrappers,
255
257
  });
256
258
  if (invoked?.effective.static !== true)
257
259
  return;
@@ -85,8 +85,8 @@ export const noUnicodeEscape = createMultiFeatureCompatRule({
85
85
  description: "Disallow `\\u`/`\\U` escapes in `$'…'` strings when the target shell does not decode them to UTF-8",
86
86
  },
87
87
  messages: {
88
- unicodeEscape: "`{{escape}}` in `$'…'` needs {{required}}, but this file targets {{target}}, which leaves it as literal text. Write the UTF-8 bytes as octal escapes instead, for example `$'{{octal}}'`.",
89
- unicodeEscapeLatin1: "`{{escape}}` in `$'…'` needs {{required}}, but this file targets {{target}}, which writes U+0080 to U+00FF as a single Latin-1 byte, not UTF-8. Write the UTF-8 bytes as octal escapes instead, for example `$'{{octal}}'`.",
88
+ unicodeEscape: "`{{escape}}` in `$'…'` needs {{required}}, but this file must support {{target}}, which leaves it as literal text. Write the UTF-8 bytes as octal escapes instead, for example `$'{{octal}}'`.",
89
+ unicodeEscapeLatin1: "`{{escape}}` in `$'…'` needs {{required}}, but this file must support {{target}}, which writes U+0080 to U+00FF as a single Latin-1 byte, not UTF-8. Write the UTF-8 bytes as octal escapes instead, for example `$'{{octal}}'`.",
90
90
  },
91
91
  },
92
92
  create(context, gates) {
@@ -16,6 +16,7 @@
16
16
  * the option word.
17
17
  */
18
18
  import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
19
+ import { resolveTransparentWrappers } from '../resolve-transparent-wrappers.js';
19
20
  import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
20
21
  import { createCompatRule } from './create-compat-rule.js';
21
22
  /**
@@ -42,15 +43,16 @@ export const noWaitN = createCompatRule({
42
43
  description: 'Disallow `wait -n` when the target shell lacks it',
43
44
  },
44
45
  messages: {
45
- waitNext: '`wait -n` needs {{required}}, but this file targets {{target}}. Wait for one job at a time instead: keep each job\'s `$!` and run `wait "$pid"`, which returns that job\'s exit status.',
46
+ waitNext: '`wait -n` needs {{required}}, but this file must support {{target}}. Wait for one job at a time instead: keep each job\'s `$!` and run `wait "$pid"`, which returns that job\'s exit status.',
46
47
  },
47
48
  },
48
49
  create(context, gate) {
49
50
  const { sourceCode } = context;
51
+ const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
50
52
  return {
51
53
  CallExpr(node) {
52
54
  const invoked = sourceCode.resolveInvokedCommand(node, {
53
- transparentWrappers: TRANSPARENT_WRAPPERS,
55
+ transparentWrappers,
54
56
  });
55
57
  if (invoked?.effective.static !== true || invoked.effective.text !== 'wait')
56
58
  return;
@@ -0,0 +1,8 @@
1
+ import type { ShellSourceCode } from '../../language/shell-source-code.js';
2
+ import type { ShellNodeOf } from '../../language/syntax-view.js';
3
+ /**
4
+ * Binds a proof query to one parsed file. The rule retains responsibility for
5
+ * selecting expansions and version gating; this helper only answers safety.
6
+ * @internal
7
+ */
8
+ export declare function createArraySafetyProof(source: ShellSourceCode): (node: ShellNodeOf<'ParamExp'>, name: string) => boolean;