@eslint-sh/eslint-plugin 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +223 -17
  2. package/dist/command-harvest/builtins/bash.d.ts +36 -0
  3. package/dist/command-harvest/builtins/bash.js +98 -0
  4. package/dist/command-harvest/builtins/bats.d.ts +45 -0
  5. package/dist/command-harvest/builtins/bats.js +33 -0
  6. package/dist/command-harvest/builtins/index.d.ts +18 -0
  7. package/dist/command-harvest/builtins/index.js +39 -0
  8. package/dist/command-harvest/builtins/posix.d.ts +51 -0
  9. package/dist/command-harvest/builtins/posix.js +93 -0
  10. package/dist/command-harvest/harvest-commands.d.ts +66 -0
  11. package/dist/command-harvest/harvest-commands.js +97 -0
  12. package/dist/command-harvest/wrapper-table.d.ts +24 -0
  13. package/dist/command-harvest/wrapper-table.js +49 -0
  14. package/dist/configs/agent-authored.d.ts +41 -0
  15. package/dist/configs/agent-authored.js +33 -0
  16. package/dist/configs/bash-3-2.d.ts +28 -0
  17. package/dist/configs/bash-3-2.js +50 -0
  18. package/dist/configs/posix-portable.d.ts +40 -0
  19. package/dist/configs/posix-portable.js +52 -0
  20. package/dist/configs/recommended.d.ts +52 -0
  21. package/dist/configs/recommended.js +34 -0
  22. package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
  23. package/dist/declaration-conventions/comment-pragma.js +153 -0
  24. package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
  25. package/dist/declaration-conventions/declaration-convention.js +1 -0
  26. package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
  27. package/dist/declaration-conventions/read-declared-commands.js +128 -0
  28. package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
  29. package/dist/eslint-plugin-syntax.d.ts +27 -0
  30. package/dist/eslint-plugin.d.ts +1211 -52
  31. package/dist/index.d.ts +56 -8
  32. package/dist/index.js +53 -6
  33. package/dist/language/command-resolution.d.ts +124 -0
  34. package/dist/language/command-resolution.js +120 -0
  35. package/dist/language/end-of-input-marker.d.ts +29 -0
  36. package/dist/language/end-of-input-marker.js +88 -0
  37. package/dist/language/line-breaks.d.ts +18 -0
  38. package/dist/language/line-breaks.js +22 -0
  39. package/dist/language/lint-view-types.d.ts +104 -0
  40. package/dist/language/lint-view-types.js +1 -0
  41. package/dist/language/non-shell-file-guard.d.ts +35 -0
  42. package/dist/language/non-shell-file-guard.js +59 -0
  43. package/dist/language/selector-classes.d.ts +0 -1
  44. package/dist/language/selector-classes.js +0 -1
  45. package/dist/language/shell-language-options.d.ts +7 -1
  46. package/dist/language/shell-language-options.js +4 -4
  47. package/dist/language/shell-language.d.ts +10 -2
  48. package/dist/language/shell-language.js +59 -8
  49. package/dist/language/shell-source-code.d.ts +342 -5
  50. package/dist/language/shell-source-code.js +559 -30
  51. package/dist/language/source-coordinates.d.ts +13 -2
  52. package/dist/language/source-coordinates.js +68 -14
  53. package/dist/language/syntax-view.d.ts +60 -39
  54. package/dist/language/syntax-view.js +200 -45
  55. package/dist/language/word-resolution.d.ts +4 -3
  56. package/dist/language/word-resolution.js +4 -3
  57. package/dist/rules/compat/create-compat-rule.d.ts +59 -0
  58. package/dist/rules/compat/create-compat-rule.js +65 -0
  59. package/dist/rules/compat/declaration-options.d.ts +19 -0
  60. package/dist/rules/compat/declaration-options.js +110 -0
  61. package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
  62. package/dist/rules/compat/no-append-all-redirect.js +73 -0
  63. package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
  64. package/dist/rules/compat/no-associative-arrays.js +39 -0
  65. package/dist/rules/compat/no-case-attributes.d.ts +12 -0
  66. package/dist/rules/compat/no-case-attributes.js +63 -0
  67. package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
  68. package/dist/rules/compat/no-case-fallthrough.js +67 -0
  69. package/dist/rules/compat/no-case-modification.d.ts +35 -0
  70. package/dist/rules/compat/no-case-modification.js +86 -0
  71. package/dist/rules/compat/no-coproc.d.ts +12 -0
  72. package/dist/rules/compat/no-coproc.js +47 -0
  73. package/dist/rules/compat/no-declare-g.d.ts +12 -0
  74. package/dist/rules/compat/no-declare-g.js +59 -0
  75. package/dist/rules/compat/no-double-bracket.d.ts +12 -0
  76. package/dist/rules/compat/no-double-bracket.js +52 -0
  77. package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
  78. package/dist/rules/compat/no-epoch-variables.js +57 -0
  79. package/dist/rules/compat/no-fd-variable.d.ts +12 -0
  80. package/dist/rules/compat/no-fd-variable.js +48 -0
  81. package/dist/rules/compat/no-globstar.d.ts +12 -0
  82. package/dist/rules/compat/no-globstar.js +39 -0
  83. package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
  84. package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
  85. package/dist/rules/compat/no-let.d.ts +12 -0
  86. package/dist/rules/compat/no-let.js +62 -0
  87. package/dist/rules/compat/no-mapfile.d.ts +13 -0
  88. package/dist/rules/compat/no-mapfile.js +122 -0
  89. package/dist/rules/compat/no-nameref.d.ts +12 -0
  90. package/dist/rules/compat/no-nameref.js +44 -0
  91. package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
  92. package/dist/rules/compat/no-negative-array-index.js +123 -0
  93. package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
  94. package/dist/rules/compat/no-nounset-empty-array.js +158 -0
  95. package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
  96. package/dist/rules/compat/no-parameter-transformation.js +123 -0
  97. package/dist/rules/compat/no-pipe-all.d.ts +13 -0
  98. package/dist/rules/compat/no-pipe-all.js +68 -0
  99. package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
  100. package/dist/rules/compat/no-printf-time-format.js +99 -0
  101. package/dist/rules/compat/no-source.d.ts +13 -0
  102. package/dist/rules/compat/no-source.js +103 -0
  103. package/dist/rules/compat/no-test-v.d.ts +12 -0
  104. package/dist/rules/compat/no-test-v.js +280 -0
  105. package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
  106. package/dist/rules/compat/no-unicode-escape.js +120 -0
  107. package/dist/rules/compat/no-wait-n.d.ts +12 -0
  108. package/dist/rules/compat/no-wait-n.js +73 -0
  109. package/dist/rules/compat/operator-range.d.ts +10 -0
  110. package/dist/rules/compat/operator-range.js +15 -0
  111. package/dist/rules/compat/posix-absent-command.d.ts +12 -0
  112. package/dist/rules/compat/posix-absent-command.js +134 -0
  113. package/dist/rules/compat/shopt-options.d.ts +9 -0
  114. package/dist/rules/compat/shopt-options.js +63 -0
  115. package/dist/rules/compat/version-gates.d.ts +76 -0
  116. package/dist/rules/compat/version-gates.js +197 -0
  117. package/dist/rules/create-rule.d.ts +91 -0
  118. package/dist/rules/create-rule.js +51 -0
  119. package/dist/rules/create-wrapper-rule.d.ts +166 -0
  120. package/dist/rules/create-wrapper-rule.js +407 -0
  121. package/dist/rules/define-shell-rule.d.ts +184 -0
  122. package/dist/rules/define-shell-rule.js +56 -0
  123. package/dist/rules/index.d.ts +26 -0
  124. package/dist/rules/index.js +88 -0
  125. package/dist/rules/max-script-complexity.d.ts +28 -0
  126. package/dist/rules/max-script-complexity.js +130 -0
  127. package/dist/rules/no-dynamic-invocation.d.ts +35 -0
  128. package/dist/rules/no-dynamic-invocation.js +117 -0
  129. package/dist/rules/no-eval.d.ts +13 -0
  130. package/dist/rules/no-eval.js +89 -0
  131. package/dist/rules/no-masked-exit-status.d.ts +21 -0
  132. package/dist/rules/no-masked-exit-status.js +667 -0
  133. package/dist/rules/no-secret-env-reads.d.ts +24 -0
  134. package/dist/rules/no-secret-env-reads.js +133 -0
  135. package/dist/rules/no-secret-flags.d.ts +32 -0
  136. package/dist/rules/no-secret-flags.js +162 -0
  137. package/dist/rules/no-unchecked-cd.d.ts +44 -0
  138. package/dist/rules/no-unchecked-cd.js +250 -0
  139. package/dist/rules/no-undeclared-commands.d.ts +20 -0
  140. package/dist/rules/no-undeclared-commands.js +115 -0
  141. package/dist/rules/no-unused-declarations.d.ts +23 -0
  142. package/dist/rules/no-unused-declarations.js +259 -0
  143. package/dist/rules/prefer-local.d.ts +16 -0
  144. package/dist/rules/prefer-local.js +249 -0
  145. package/dist/rules/quote-expansions.d.ts +13 -0
  146. package/dist/rules/quote-expansions.js +229 -0
  147. package/dist/rules/require-network-wrapper.d.ts +40 -0
  148. package/dist/rules/require-network-wrapper.js +244 -0
  149. package/dist/rules/require-shebang.d.ts +29 -0
  150. package/dist/rules/require-shebang.js +101 -0
  151. package/dist/rules/require-strict-mode.d.ts +52 -0
  152. package/dist/rules/require-strict-mode.js +177 -0
  153. package/dist/rules/require-tty-guard.d.ts +13 -0
  154. package/dist/rules/require-tty-guard.js +289 -0
  155. package/dist/rules/resolve-declaration-convention.d.ts +96 -0
  156. package/dist/rules/resolve-declaration-convention.js +109 -0
  157. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  158. package/dist/rules/resolve-network-wrapper.js +116 -0
  159. package/dist/rules/set-options.d.ts +109 -0
  160. package/dist/rules/set-options.js +201 -0
  161. package/dist/rules/shebang-interpreter.d.ts +18 -0
  162. package/dist/rules/shebang-interpreter.js +81 -0
  163. package/dist/rules/shebang-matches-dialect.d.ts +12 -0
  164. package/dist/rules/shebang-matches-dialect.js +69 -0
  165. package/dist/rules/transparent-wrappers.d.ts +52 -0
  166. package/dist/rules/transparent-wrappers.js +99 -0
  167. package/dist/rules/user-pattern.d.ts +63 -0
  168. package/dist/rules/user-pattern.js +184 -0
  169. package/dist/syntax/analyze.d.ts +19 -0
  170. package/dist/syntax/analyze.js +21 -0
  171. package/dist/syntax/index.d.ts +23 -0
  172. package/dist/syntax/index.js +25 -0
  173. package/dist/version.d.ts +0 -1
  174. package/dist/version.js +0 -1
  175. package/docs/compat.md +347 -0
  176. package/docs/custom-rule-tutorial.md +780 -34
  177. package/docs/declaration-conventions.md +478 -0
  178. package/docs/examples/audit-wrapper-pack.mjs +15 -0
  179. package/docs/examples/dep-line-convention.mjs +22 -0
  180. package/docs/examples/eslint.config.mjs +3 -2
  181. package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
  182. package/docs/examples/no-backticks-rule-tester.mjs +106 -0
  183. package/docs/examples/no-backticks-rule.mjs +60 -0
  184. package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
  185. package/docs/examples/no-function-keyword-rule.mjs +59 -0
  186. package/docs/examples/no-listed-commands-rule.ts +63 -0
  187. package/docs/examples/no-rm-rf-rule.mjs +26 -15
  188. package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
  189. package/docs/examples/sidecar-convention.mjs +38 -0
  190. package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
  191. package/docs/examples/sorted-declarations-rule.mjs +52 -0
  192. package/docs/reading-nodes.md +159 -0
  193. package/docs/rules/README.md +78 -0
  194. package/docs/rules/_template.md +83 -0
  195. package/docs/rules/compat/no-append-all-redirect.md +87 -0
  196. package/docs/rules/compat/no-associative-arrays.md +95 -0
  197. package/docs/rules/compat/no-case-attributes.md +82 -0
  198. package/docs/rules/compat/no-case-fallthrough.md +90 -0
  199. package/docs/rules/compat/no-case-modification.md +74 -0
  200. package/docs/rules/compat/no-coproc.md +76 -0
  201. package/docs/rules/compat/no-declare-g.md +80 -0
  202. package/docs/rules/compat/no-double-bracket.md +72 -0
  203. package/docs/rules/compat/no-epoch-variables.md +79 -0
  204. package/docs/rules/compat/no-fd-variable.md +78 -0
  205. package/docs/rules/compat/no-globstar.md +74 -0
  206. package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
  207. package/docs/rules/compat/no-let.md +78 -0
  208. package/docs/rules/compat/no-mapfile.md +118 -0
  209. package/docs/rules/compat/no-nameref.md +81 -0
  210. package/docs/rules/compat/no-negative-array-index.md +87 -0
  211. package/docs/rules/compat/no-nounset-empty-array.md +103 -0
  212. package/docs/rules/compat/no-parameter-transformation.md +94 -0
  213. package/docs/rules/compat/no-pipe-all.md +85 -0
  214. package/docs/rules/compat/no-printf-time-format.md +75 -0
  215. package/docs/rules/compat/no-source.md +98 -0
  216. package/docs/rules/compat/no-test-v.md +82 -0
  217. package/docs/rules/compat/no-unicode-escape.md +91 -0
  218. package/docs/rules/compat/no-wait-n.md +85 -0
  219. package/docs/rules/max-script-complexity.md +109 -0
  220. package/docs/rules/no-dynamic-invocation.md +139 -0
  221. package/docs/rules/no-eval.md +106 -0
  222. package/docs/rules/no-masked-exit-status.md +239 -0
  223. package/docs/rules/no-secret-env-reads.md +173 -0
  224. package/docs/rules/no-secret-flags.md +169 -0
  225. package/docs/rules/no-unchecked-cd.md +146 -0
  226. package/docs/rules/no-undeclared-commands.md +141 -0
  227. package/docs/rules/no-unused-declarations.md +216 -0
  228. package/docs/rules/prefer-local.md +221 -0
  229. package/docs/rules/quote-expansions.md +157 -0
  230. package/docs/rules/require-network-wrapper.md +171 -0
  231. package/docs/rules/require-shebang.md +157 -0
  232. package/docs/rules/require-strict-mode.md +253 -0
  233. package/docs/rules/require-tty-guard.md +128 -0
  234. package/docs/rules/shebang-matches-dialect.md +120 -0
  235. package/docs/selectors.md +2 -2
  236. package/docs/shellcheck.md +86 -0
  237. package/docs/wrapper-rules.md +206 -0
  238. package/package.json +26 -20
  239. package/dist/index.d.ts.map +0 -1
  240. package/dist/index.js.map +0 -1
  241. package/dist/language/selector-classes.d.ts.map +0 -1
  242. package/dist/language/selector-classes.js.map +0 -1
  243. package/dist/language/shell-language-options.d.ts.map +0 -1
  244. package/dist/language/shell-language-options.js.map +0 -1
  245. package/dist/language/shell-language.d.ts.map +0 -1
  246. package/dist/language/shell-language.js.map +0 -1
  247. package/dist/language/shell-source-code.d.ts.map +0 -1
  248. package/dist/language/shell-source-code.js.map +0 -1
  249. package/dist/language/source-coordinates.d.ts.map +0 -1
  250. package/dist/language/source-coordinates.js.map +0 -1
  251. package/dist/language/syntax-view.d.ts.map +0 -1
  252. package/dist/language/syntax-view.js.map +0 -1
  253. package/dist/language/word-resolution.d.ts.map +0 -1
  254. package/dist/language/word-resolution.js.map +0 -1
  255. package/dist/version.d.ts.map +0 -1
  256. package/dist/version.js.map +0 -1
@@ -0,0 +1,59 @@
1
+ import type { BuiltInRuleMeta } from '../create-rule.js';
2
+ import type { ShellRule, ShellRuleContext, ShellRuleVisitor } from '../define-shell-rule.js';
3
+ import type { CompatFeature, GateDescription } from './version-gates.js';
4
+ /**
5
+ * What a compat rule hands to {@link createCompatRule}.
6
+ *
7
+ * @internal
8
+ */
9
+ export interface CompatRuleSpec<MessageIds extends string> {
10
+ /**
11
+ * The registry name, always under the `compat/` namespace
12
+ * (`compat/no-mapfile`, written `sh/compat/no-mapfile` in a config). Its
13
+ * docs page is `docs/rules/compat/<name>.md`.
14
+ */
15
+ readonly name: `compat/${string}`;
16
+ /** The gate-table row this rule enforces. */
17
+ readonly feature: CompatFeature;
18
+ /** The rule's metadata; `schema` is fixed to `[]` by the factory. */
19
+ readonly meta: Omit<BuiltInRuleMeta<[], MessageIds>, 'schema'>;
20
+ /**
21
+ * Builds the listeners for a file in which the feature is unavailable.
22
+ * `gate` describes the requirement and the file's target for messages.
23
+ */
24
+ readonly create: (context: ShellRuleContext<[], MessageIds>, gate: GateDescription) => ShellRuleVisitor;
25
+ }
26
+ /**
27
+ * Builds a `compat/*` rule: a built-in rule (through `createRule`) that is
28
+ * a no-op unless the file's `languageOptions` make `spec.feature`
29
+ * unavailable.
30
+ *
31
+ * @internal
32
+ */
33
+ export declare function createCompatRule<MessageIds extends string>(spec: CompatRuleSpec<MessageIds>): ShellRule<[], MessageIds>;
34
+ /**
35
+ * What a compat rule that reports several gate-table rows hands to
36
+ * {@link createMultiFeatureCompatRule}: one rule for one construct whose
37
+ * minimum version depends on how it is written (such as `${v@op}`, where
38
+ * the operator decides the release).
39
+ *
40
+ * @internal
41
+ */
42
+ export interface MultiFeatureCompatRuleSpec<MessageIds extends string> extends Omit<CompatRuleSpec<MessageIds>, 'feature' | 'create'> {
43
+ /** The gate-table rows this rule enforces. */
44
+ readonly features: readonly CompatFeature[];
45
+ /**
46
+ * Builds the listeners for a file in which at least one of `features` is
47
+ * unavailable. `gates` holds a {@link GateDescription} for exactly the
48
+ * unavailable ones, so a feature missing from it must not be reported.
49
+ */
50
+ readonly create: (context: ShellRuleContext<[], MessageIds>, gates: ReadonlyMap<CompatFeature, GateDescription>) => ShellRuleVisitor;
51
+ }
52
+ /**
53
+ * Builds a `compat/*` rule over several gate-table rows: a no-op unless the
54
+ * file's `languageOptions` make at least one of `spec.features`
55
+ * unavailable, and then given the description of each unavailable one.
56
+ *
57
+ * @internal
58
+ */
59
+ export declare function createMultiFeatureCompatRule<MessageIds extends string>(spec: MultiFeatureCompatRuleSpec<MessageIds>): ShellRule<[], MessageIds>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * `createCompatRule`: the factory every `compat/*` rule is built with.
3
+ *
4
+ * A compat rule reports one gated feature (a row of the gate table in
5
+ * `./version-gates.ts`) wherever a file uses it while its `languageOptions`
6
+ * say the feature is unavailable. The factory owns the gate decision, so no
7
+ * compat rule reads `shellVersion` or `dialect` itself:
8
+ *
9
+ * - when {@link isFeatureAvailable} says the feature is available, the rule
10
+ * returns no listeners at all, so it costs nothing and cannot report;
11
+ * - otherwise the rule's own `create()` runs and receives the
12
+ * {@link GateDescription} (`required`, `target`) to interpolate into its
13
+ * messages as `{{required}}` and `{{target}}`.
14
+ *
15
+ * Compat rules have no options: what they gate on is the language options,
16
+ * which the user already sets once for the whole file set.
17
+ */
18
+ import { createRule } from '../create-rule.js';
19
+ import { describeGate, isFeatureAvailable } from './version-gates.js';
20
+ /**
21
+ * Builds a `compat/*` rule: a built-in rule (through `createRule`) that is
22
+ * a no-op unless the file's `languageOptions` make `spec.feature`
23
+ * unavailable.
24
+ *
25
+ * @internal
26
+ */
27
+ export function createCompatRule(spec) {
28
+ const { name, feature, meta, create } = spec;
29
+ return createRule({
30
+ name,
31
+ meta: { ...meta, schema: [] },
32
+ create(context) {
33
+ const { languageOptions } = context;
34
+ if (isFeatureAvailable(feature, languageOptions))
35
+ return {};
36
+ return create(context, describeGate(feature, languageOptions));
37
+ },
38
+ });
39
+ }
40
+ /**
41
+ * Builds a `compat/*` rule over several gate-table rows: a no-op unless the
42
+ * file's `languageOptions` make at least one of `spec.features`
43
+ * unavailable, and then given the description of each unavailable one.
44
+ *
45
+ * @internal
46
+ */
47
+ export function createMultiFeatureCompatRule(spec) {
48
+ const { name, features, meta, create } = spec;
49
+ return createRule({
50
+ name,
51
+ meta: { ...meta, schema: [] },
52
+ create(context) {
53
+ const { languageOptions } = context;
54
+ const gates = new Map();
55
+ for (const feature of features) {
56
+ if (!isFeatureAvailable(feature, languageOptions)) {
57
+ gates.set(feature, describeGate(feature, languageOptions));
58
+ }
59
+ }
60
+ if (gates.size === 0)
61
+ return {};
62
+ return create(context, gates);
63
+ },
64
+ });
65
+ }
@@ -0,0 +1,19 @@
1
+ import type { ShellSourceCode } from '../../language/shell-source-code.js';
2
+ import type { ShellNode } from '../../language/syntax-view.js';
3
+ import type { ShellRuleVisitor } from '../define-shell-rule.js';
4
+ /**
5
+ * `declare`, `typeset` and `local`: the commands for an option that `local`
6
+ * accepts as well, which is every `declare` option except `-g`, `-f` and
7
+ * `-F`.
8
+ *
9
+ * @internal
10
+ */
11
+ export declare const DECLARATION_AND_LOCAL_COMMANDS: ReadonlySet<string>;
12
+ /**
13
+ * Builds the listeners that call `report(optionWord, command, optionWords)` for each
14
+ * `declare` / `typeset` (or each of `commands`) whose option cluster contains
15
+ * `flag`: one letter, or a character class such as `[lu]` for either of two.
16
+ *
17
+ * @internal
18
+ */
19
+ export declare function declarationFlagVisitor(sourceCode: ShellSourceCode, flag: string, report: (option: ShellNode, command: string, options: readonly string[]) => void, commands?: ReadonlySet<string>): ShellRuleVisitor;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Shared detection of an option flag on `declare` / `typeset`, for the compat
3
+ * rules that gate one flag of those builtins (`-A` associative arrays, `-g`
4
+ * global scope).
5
+ *
6
+ * What is read: the option words of a `declare` or `typeset` command, up to
7
+ * its first operand (a name, `--`, or a word that is not static), exactly as
8
+ * the builtin's option parser stops. An option word matches when it is a `-`
9
+ * cluster containing the flag (`-A`, `-gA`, `-Ar`). The command is found both
10
+ * as Bash's declaration clause and as an ordinary command (POSIX mode parses
11
+ * `declare -A m` as one, and so does a wrapper such as `builtin declare -A m`).
12
+ * By default `local`, `readonly` and `export` are outside scope; a caller
13
+ * gating an option `local` shares (`-n`, `-l`, `-u`) adds `local` with
14
+ * {@link DECLARATION_AND_LOCAL_COMMANDS}.
15
+ */
16
+ import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
17
+ import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
18
+ /** The declaration commands that take option clusters. */
19
+ const DECLARATION_COMMANDS = new Set(['declare', 'typeset']);
20
+ /**
21
+ * `declare`, `typeset` and `local`: the commands for an option that `local`
22
+ * accepts as well, which is every `declare` option except `-g`, `-f` and
23
+ * `-F`.
24
+ *
25
+ * @internal
26
+ */
27
+ export const DECLARATION_AND_LOCAL_COMMANDS = new Set([
28
+ 'declare',
29
+ 'typeset',
30
+ 'local',
31
+ ]);
32
+ /**
33
+ * The wrappers followed to find a declaration command written as an ordinary
34
+ * command: `sh-ast/analyze`'s defaults (which include `command`) plus Bash's
35
+ * `builtin`.
36
+ */
37
+ const TRANSPARENT_WRAPPERS = [
38
+ ...DEFAULT_TRANSPARENT_WRAPPERS,
39
+ BUILTIN_WRAPPER,
40
+ ];
41
+ /**
42
+ * Finds the option word carrying `flag` among a declaration command's
43
+ * arguments (the words after the command name), together with all of the
44
+ * command's option words, or `undefined` when no option word carries it.
45
+ * Option processing stops at the first operand, at `--`, and at a word that
46
+ * is not static, because what follows it may not be an option.
47
+ */
48
+ function findFlagOption(sourceCode, flagPattern, args) {
49
+ let report;
50
+ const options = [];
51
+ for (const { report: node, word } of args) {
52
+ if (word === undefined)
53
+ break;
54
+ const text = sourceCode.resolveWordText(word);
55
+ if (text === null || text === '--' || !/^[-+]./u.test(text))
56
+ break;
57
+ options.push(text);
58
+ if (report === undefined && flagPattern.test(text))
59
+ report = node;
60
+ }
61
+ return report === undefined ? undefined : { report, options };
62
+ }
63
+ /**
64
+ * The arguments of a `DeclClause` as candidates. An option such as `-A` is an
65
+ * `Assign` with no `name` and the option word as its `value`; an `Assign` that
66
+ * has a `name` is an operand and contributes no word.
67
+ */
68
+ function declClauseArgs(node) {
69
+ return node.args.map((assign) => {
70
+ const word = assign.name === undefined ? assign.value : undefined;
71
+ return { report: word ?? assign, word };
72
+ });
73
+ }
74
+ /**
75
+ * Builds the listeners that call `report(optionWord, command, optionWords)` for each
76
+ * `declare` / `typeset` (or each of `commands`) whose option cluster contains
77
+ * `flag`: one letter, or a character class such as `[lu]` for either of two.
78
+ *
79
+ * @internal
80
+ */
81
+ export function declarationFlagVisitor(sourceCode, flag, report, commands = DECLARATION_COMMANDS) {
82
+ const flagPattern = new RegExp(`^-[a-zA-Z]*${flag}[a-zA-Z]*$`, 'u');
83
+ return {
84
+ DeclClause(node) {
85
+ const command = node.variant?.value;
86
+ if (command === undefined || !commands.has(command))
87
+ return;
88
+ const option = findFlagOption(sourceCode, flagPattern, declClauseArgs(node));
89
+ if (option !== undefined)
90
+ report(option.report, command, option.options);
91
+ },
92
+ CallExpr(node) {
93
+ const invoked = sourceCode.resolveInvokedCommand(node, {
94
+ transparentWrappers: TRANSPARENT_WRAPPERS,
95
+ });
96
+ if (invoked?.effective.static !== true)
97
+ return;
98
+ const command = invoked.effective.text;
99
+ if (!commands.has(command))
100
+ return;
101
+ // `chainIndices` index `node.args` directly, so slice before filtering.
102
+ const operands = node.args
103
+ .slice((invoked.chainIndices.at(-1) ?? 0) + 1)
104
+ .map((word) => ({ report: word, word }));
105
+ const option = findFlagOption(sourceCode, flagPattern, operands);
106
+ if (option !== undefined)
107
+ report(option.report, command, option.options);
108
+ },
109
+ };
110
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Message ids this rule reports with: `appendAll` for the report and
3
+ * `useAppendRedirects` for its suggestion.
4
+ *
5
+ * @internal
6
+ */
7
+ export type NoAppendAllRedirectMessageId = 'appendAll' | 'useAppendRedirects';
8
+ /**
9
+ * The `sh/compat/no-append-all-redirect` rule module.
10
+ *
11
+ * @internal
12
+ */
13
+ export declare const noAppendAllRedirect: import("../define-shell-rule.js").ShellRule<[], NoAppendAllRedirectMessageId>;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `sh/compat/no-append-all-redirect`: reports the `&>>` redirection, which
3
+ * appends both stdout and stderr to a file, in files whose target shell
4
+ * lacks it, that is, Bash older than 4.0 (macOS's stock `/bin/bash` is 3.2)
5
+ * and POSIX sh.
6
+ *
7
+ * Bash 3.2 rejects `&>>` as a syntax error, so the whole script fails to
8
+ * run. POSIX sh reads `cmd &>> file` as `cmd &` (run in the background)
9
+ * followed by an empty command that appends nothing to `file`. The portable
10
+ * spelling is `>> file 2>&1`, which the Bash manual defines `&>>` to mean.
11
+ *
12
+ * The rule offers that rewrite as a suggestion: the operator becomes `>>`
13
+ * and ` 2>&1` follows the target word, so the redirection keeps its place
14
+ * among the command's other redirections.
15
+ *
16
+ * What is read: a `Redirect` whose operator is `&>>`. The two-way `&>` and
17
+ * the plain `>>`, `>&` and `2>&1` are different operators and are not
18
+ * reported, and neither is any of them inside quotes. The report is on the
19
+ * whole redirection.
20
+ */
21
+ import { AppAll } from 'sh-ast';
22
+ import { createCompatRule } from './create-compat-rule.js';
23
+ /** The operator as written. */
24
+ const OPERATOR = '&>>';
25
+ /**
26
+ * The `sh/compat/no-append-all-redirect` rule module.
27
+ *
28
+ * @internal
29
+ */
30
+ export const noAppendAllRedirect = createCompatRule({
31
+ name: 'compat/no-append-all-redirect',
32
+ feature: 'appendAllRedirect',
33
+ meta: {
34
+ type: 'problem',
35
+ hasSuggestions: true,
36
+ docs: {
37
+ description: 'Disallow the `&>>` redirection when the target shell lacks it',
38
+ },
39
+ messages: {
40
+ appendAll: '`&>>` needs {{required}}, but this file targets {{target}}. Append both streams with `>> file 2>&1` instead.',
41
+ useAppendRedirects: 'Replace `&>> {{file}}` with `>> {{file}} 2>&1`.',
42
+ },
43
+ },
44
+ create(context, gate) {
45
+ const { sourceCode } = context;
46
+ return {
47
+ Redirect(node) {
48
+ if (node.op !== AppAll)
49
+ return;
50
+ const [start] = sourceCode.getRange(node);
51
+ const { word } = node;
52
+ const exact = word !== undefined && sourceCode.text.startsWith(OPERATOR, start);
53
+ context.report({
54
+ node,
55
+ messageId: 'appendAll',
56
+ data: { ...gate },
57
+ suggest: exact
58
+ ? [
59
+ {
60
+ messageId: 'useAppendRedirects',
61
+ data: { file: sourceCode.getText(word) },
62
+ fix: (fixer) => [
63
+ fixer.replaceTextRange([start, start + OPERATOR.length], '>>'),
64
+ fixer.insertTextAfter(word, ' 2>&1'),
65
+ ],
66
+ },
67
+ ]
68
+ : [],
69
+ });
70
+ },
71
+ };
72
+ },
73
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoAssociativeArraysMessageId = 'associativeArray';
7
+ /**
8
+ * The `sh/compat/no-associative-arrays` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noAssociativeArrays: import("../define-shell-rule.js").ShellRule<[], "associativeArray">;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `sh/compat/no-associative-arrays`: reports associative-array declarations
3
+ * (`declare -A`, `typeset -A`) in files whose target shell lacks them, that
4
+ * is, Bash older than 4.0 (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
5
+ *
6
+ * Bash 3.2 rejects `-A` as an invalid option, so the script fails where the
7
+ * array is declared, and every later `${map[key]}` silently indexes an
8
+ * ordinary array by the arithmetic value of `key`. The message suggests the
9
+ * portable alternative: move the keyed lookup into `jq` over a JSON object.
10
+ *
11
+ * What is read: `declare`/`typeset` option clusters containing `A` (`-A`,
12
+ * `-gA`, `-Ar`), found as `declarationFlagVisitor` defines. `local -A`,
13
+ * `readonly -A` and `export -A` are outside this rule's scope.
14
+ */
15
+ import { createCompatRule } from './create-compat-rule.js';
16
+ import { declarationFlagVisitor } from './declaration-options.js';
17
+ /**
18
+ * The `sh/compat/no-associative-arrays` rule module.
19
+ *
20
+ * @internal
21
+ */
22
+ export const noAssociativeArrays = createCompatRule({
23
+ name: 'compat/no-associative-arrays',
24
+ feature: 'associativeArrays',
25
+ meta: {
26
+ type: 'problem',
27
+ docs: {
28
+ description: 'Disallow associative arrays (`declare -A`, `typeset -A`) when the target shell lacks them',
29
+ },
30
+ messages: {
31
+ associativeArray: 'Associative arrays (`{{command}} -A`) need {{required}}, but this file targets {{target}}. Consider moving the keyed lookup into `jq`, for example `jq -r --arg k "$key" \'.[$k]\' map.json`.',
32
+ },
33
+ },
34
+ create(context, gate) {
35
+ return declarationFlagVisitor(context.sourceCode, 'A', (node, command) => {
36
+ context.report({ node, messageId: 'associativeArray', data: { ...gate, command } });
37
+ });
38
+ },
39
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoCaseAttributesMessageId = 'caseAttribute' | 'caseAttributesBoth';
7
+ /**
8
+ * The `sh/compat/no-case-attributes` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noCaseAttributes: import("../define-shell-rule.js").ShellRule<[], NoCaseAttributesMessageId>;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * `sh/compat/no-case-attributes`: reports the case-converting attributes
3
+ * `declare -l` and `declare -u` (also through `typeset` and `local`) in files
4
+ * whose target shell lacks them, that is, Bash older than 4.0 (macOS's stock
5
+ * `/bin/bash` is 3.2) and POSIX sh.
6
+ *
7
+ * Bash 3.2 rejects the option, so the declaration fails and the variable is
8
+ * left unset. The message suggests the portable alternative: convert the
9
+ * value with `tr` when assigning it.
10
+ *
11
+ * What is read: `declare`/`typeset`/`local` option clusters containing `l`
12
+ * or `u` (`-l`, `-u`, `-lx`), found as `declarationFlagVisitor` defines. The
13
+ * report is on the option word. A cluster with both letters (`-lu`) reports
14
+ * once, with its own message: on every Bash that has the attributes, `-l`
15
+ * and `-u` together cancel out and leave the value as written (observed on
16
+ * Bash 4.0 and 5.3), so no `tr` conversion matches it.
17
+ */
18
+ import { createCompatRule } from './create-compat-rule.js';
19
+ import { DECLARATION_AND_LOCAL_COMMANDS, declarationFlagVisitor } from './declaration-options.js';
20
+ /** The `tr` conversion that gives each attribute's result. */
21
+ const TR_FOR = {
22
+ l: "tr '[:upper:]' '[:lower:]'",
23
+ u: "tr '[:lower:]' '[:upper:]'",
24
+ };
25
+ /**
26
+ * The `sh/compat/no-case-attributes` rule module.
27
+ *
28
+ * @internal
29
+ */
30
+ export const noCaseAttributes = createCompatRule({
31
+ name: 'compat/no-case-attributes',
32
+ feature: 'caseAttributes',
33
+ meta: {
34
+ type: 'problem',
35
+ docs: {
36
+ description: 'Disallow the case attributes `declare -l` and `declare -u` when the target shell lacks them',
37
+ },
38
+ messages: {
39
+ caseAttribute: '`{{command}} -{{flag}}` needs {{required}}, but this file targets {{target}}. Convert the value when you assign it instead, for example `v=$(printf \'%s\' "$v" | {{tr}})`.',
40
+ caseAttributesBoth: '`{{command}} {{option}}` needs {{required}}, but this file targets {{target}}. Where it is supported, `-l` and `-u` together cancel out and leave the value as written, so drop both letters.',
41
+ },
42
+ },
43
+ create(context, gate) {
44
+ const { sourceCode } = context;
45
+ return declarationFlagVisitor(sourceCode, '[lu]', (node, command) => {
46
+ const option = (node.type === 'Word' ? sourceCode.resolveWordText(node) : null) ?? '';
47
+ if (option.includes('l') && option.includes('u')) {
48
+ context.report({
49
+ node,
50
+ messageId: 'caseAttributesBoth',
51
+ data: { ...gate, command, option },
52
+ });
53
+ return;
54
+ }
55
+ const flag = option.includes('l') ? 'l' : 'u';
56
+ context.report({
57
+ node,
58
+ messageId: 'caseAttribute',
59
+ data: { ...gate, command, flag, tr: TR_FOR[flag] },
60
+ });
61
+ }, DECLARATION_AND_LOCAL_COMMANDS);
62
+ },
63
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoCaseFallthroughMessageId = 'fallthrough' | 'resume';
7
+ /**
8
+ * The `sh/compat/no-case-fallthrough` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noCaseFallthrough: import("../define-shell-rule.js").ShellRule<[], NoCaseFallthroughMessageId>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `sh/compat/no-case-fallthrough`: reports the `case` terminators `;&` (run
3
+ * the next branch without testing its pattern) and `;;&` (go on testing the
4
+ * next patterns) in files whose target shell lacks them, that is, Bash older
5
+ * than 4.0 (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
6
+ *
7
+ * Bash 3.2 rejects both as a syntax error, so the whole script fails to run.
8
+ * POSIX.1-2024 adds `;&`, but dash 0.5.12 still rejects it, so a POSIX
9
+ * target is treated as lacking it. The message suggests the portable
10
+ * rewrite: for `;&`, repeat the next branch's commands at the end of this
11
+ * one and end it with `;;`; for `;;&`, move the patterns after this branch
12
+ * into a second `case` statement.
13
+ *
14
+ * What is read: a `case` branch (`CaseItem`) whose terminator is `;&` or
15
+ * `;;&`. The usual `;;`, and these characters inside quotes or a pattern,
16
+ * are not reported. The report is on the terminator.
17
+ */
18
+ import { Fallthrough, Resume } from 'sh-ast';
19
+ import { createCompatRule } from './create-compat-rule.js';
20
+ import { operatorRange } from './operator-range.js';
21
+ /** Each terminator's message and spelling. */
22
+ const TERMINATORS = new Map([
23
+ [Fallthrough, { messageId: 'fallthrough', text: ';&' }],
24
+ [Resume, { messageId: 'resume', text: ';;&' }],
25
+ ]);
26
+ /**
27
+ * The `sh/compat/no-case-fallthrough` rule module.
28
+ *
29
+ * @internal
30
+ */
31
+ export const noCaseFallthrough = createCompatRule({
32
+ name: 'compat/no-case-fallthrough',
33
+ feature: 'caseFallthrough',
34
+ meta: {
35
+ type: 'problem',
36
+ docs: {
37
+ description: 'Disallow the `case` terminators `;&` and `;;&` when the target shell lacks them',
38
+ },
39
+ messages: {
40
+ fallthrough: "`;&` needs {{required}}, but this file targets {{target}}. Repeat the next branch's commands at the end of this branch and end it with `;;` instead.",
41
+ resume: '`;;&` needs {{required}}, but this file targets {{target}}. End this branch with `;;` and test the patterns that follow it in a second `case` statement instead.',
42
+ },
43
+ },
44
+ create(context, gate) {
45
+ const { sourceCode } = context;
46
+ return {
47
+ CaseItem(node) {
48
+ const terminator = TERMINATORS.get(node.op);
49
+ if (terminator === undefined)
50
+ return;
51
+ const range = operatorRange(sourceCode, node, terminator.text);
52
+ if (range === undefined) {
53
+ context.report({ node, messageId: terminator.messageId, data: { ...gate } });
54
+ return;
55
+ }
56
+ context.report({
57
+ loc: {
58
+ start: sourceCode.getLocFromIndex(range[0]),
59
+ end: sourceCode.getLocFromIndex(range[1]),
60
+ },
61
+ messageId: terminator.messageId,
62
+ data: { ...gate },
63
+ });
64
+ },
65
+ };
66
+ },
67
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoCaseModificationMessageId = 'caseModification' | 'caseModificationPattern';
7
+ /**
8
+ * What `tr` is given to convert, and to which case, for one operator.
9
+ *
10
+ * @internal
11
+ */
12
+ export interface CaseOperator {
13
+ /** The operator as written after the name. */
14
+ readonly operator: string;
15
+ /** Whether the operator converts every character (`^^`, `,,`) or the first. */
16
+ readonly all: boolean;
17
+ /** `tr` arguments that perform the conversion. */
18
+ readonly tr: string;
19
+ }
20
+ /** `tr` arguments that convert to upper case. @internal */
21
+ export declare const TO_UPPER = "tr '[:lower:]' '[:upper:]'";
22
+ /** `tr` arguments that convert to lower case. @internal */
23
+ export declare const TO_LOWER = "tr '[:upper:]' '[:lower:]'";
24
+ /**
25
+ * The `tr` replacement shown in the message for `name` under `spec`.
26
+ *
27
+ * @internal
28
+ */
29
+ export declare function trExample(name: string, spec: CaseOperator): string;
30
+ /**
31
+ * The `sh/compat/no-case-modification` rule module.
32
+ *
33
+ * @internal
34
+ */
35
+ export declare const noCaseModification: import("../define-shell-rule.js").ShellRule<[], NoCaseModificationMessageId>;