@eslint-sh/eslint-plugin 0.7.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 (141) hide show
  1. package/README.md +14 -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 +4 -3
  10. package/dist/configs/posix-portable.d.ts +1 -1
  11. package/dist/declaration-conventions/read-declared-commands.d.ts +2 -2
  12. package/dist/declaration-conventions/read-declared-commands.js +2 -2
  13. package/dist/eslint-plugin-syntax.d.ts +4 -0
  14. package/dist/eslint-plugin.d.ts +86 -40
  15. package/dist/index.d.ts +5 -2
  16. package/dist/index.js +4 -2
  17. package/dist/language/shell-language-options.d.ts +18 -15
  18. package/dist/language/shell-language-options.js +33 -26
  19. package/dist/language/shell-language.d.ts +10 -9
  20. package/dist/language/shell-language.js +18 -35
  21. package/dist/language/shell-source-code.d.ts +17 -9
  22. package/dist/language/shell-source-code.js +53 -12
  23. package/dist/rules/bash-variable-semantics.d.ts +22 -0
  24. package/dist/rules/bash-variable-semantics.js +41 -0
  25. package/dist/rules/compat/create-compat-rule.js +1 -1
  26. package/dist/rules/compat/declaration-options.d.ts +2 -2
  27. package/dist/rules/compat/declaration-options.js +5 -2
  28. package/dist/rules/compat/no-append-all-redirect.js +1 -1
  29. package/dist/rules/compat/no-associative-arrays.js +2 -2
  30. package/dist/rules/compat/no-brace-sequence-padding-step.js +4 -4
  31. package/dist/rules/compat/no-case-attributes.js +3 -3
  32. package/dist/rules/compat/no-case-fallthrough.js +2 -2
  33. package/dist/rules/compat/no-case-modification.js +2 -2
  34. package/dist/rules/compat/no-coproc.js +1 -1
  35. package/dist/rules/compat/no-declare-g.js +3 -3
  36. package/dist/rules/compat/no-double-bracket.js +2 -2
  37. package/dist/rules/compat/no-epoch-variables.js +2 -2
  38. package/dist/rules/compat/no-fd-variable.js +1 -1
  39. package/dist/rules/compat/no-globstar.js +1 -1
  40. package/dist/rules/compat/no-lastpipe-semantics.js +2 -2
  41. package/dist/rules/compat/no-let.js +2 -1
  42. package/dist/rules/compat/no-mapfile.js +4 -2
  43. package/dist/rules/compat/no-nameref.js +3 -3
  44. package/dist/rules/compat/no-negative-array-index.js +39 -9
  45. package/dist/rules/compat/no-nounset-empty-array.js +15 -4
  46. package/dist/rules/compat/no-parameter-transformation.js +2 -2
  47. package/dist/rules/compat/no-pipe-all.js +1 -1
  48. package/dist/rules/compat/no-printf-time-format.js +4 -2
  49. package/dist/rules/compat/no-source.js +2 -1
  50. package/dist/rules/compat/no-test-v.js +4 -2
  51. package/dist/rules/compat/no-unicode-escape.js +2 -2
  52. package/dist/rules/compat/no-wait-n.js +4 -2
  53. package/dist/rules/compat/nounset-array-proof.d.ts +8 -0
  54. package/dist/rules/compat/nounset-array-proof.js +563 -0
  55. package/dist/rules/compat/posix-absent-command.d.ts +7 -8
  56. package/dist/rules/compat/posix-absent-command.js +17 -15
  57. package/dist/rules/compat/shopt-options.js +3 -1
  58. package/dist/rules/compat/version-gates.d.ts +9 -9
  59. package/dist/rules/compat/version-gates.js +13 -13
  60. package/dist/rules/create-wrapper-rule.js +12 -7
  61. package/dist/rules/no-dynamic-invocation.d.ts +0 -8
  62. package/dist/rules/no-dynamic-invocation.js +7 -15
  63. package/dist/rules/no-eval.js +3 -1
  64. package/dist/rules/no-masked-exit-status.js +15 -8
  65. package/dist/rules/no-secret-env-reads.d.ts +4 -3
  66. package/dist/rules/no-secret-env-reads.js +37 -14
  67. package/dist/rules/no-secret-flags.d.ts +2 -1
  68. package/dist/rules/no-secret-flags.js +7 -8
  69. package/dist/rules/no-unchecked-cd.js +3 -1
  70. package/dist/rules/no-undeclared-commands.d.ts +1 -15
  71. package/dist/rules/no-undeclared-commands.js +3 -15
  72. package/dist/rules/no-unused-declarations.d.ts +1 -18
  73. package/dist/rules/no-unused-declarations.js +9 -34
  74. package/dist/rules/prefer-local.js +4 -3
  75. package/dist/rules/quote-expansions.js +3 -28
  76. package/dist/rules/require-network-wrapper.d.ts +2 -17
  77. package/dist/rules/require-network-wrapper.js +33 -4
  78. package/dist/rules/resolve-declaration-convention.d.ts +5 -6
  79. package/dist/rules/resolve-declaration-convention.js +5 -6
  80. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  81. package/dist/rules/resolve-network-wrapper.js +57 -26
  82. package/dist/rules/resolve-transparent-wrappers.d.ts +43 -0
  83. package/dist/rules/resolve-transparent-wrappers.js +98 -0
  84. package/dist/rules/set-options.d.ts +2 -1
  85. package/dist/rules/set-options.js +17 -5
  86. package/dist/rules/shebang-interpreter.js +4 -2
  87. package/dist/rules/transparent-wrappers.d.ts +4 -3
  88. package/dist/rules/transparent-wrappers.js +5 -4
  89. package/dist/rules/user-pattern.d.ts +5 -3
  90. package/dist/rules/user-pattern.js +6 -4
  91. package/dist/syntax/index.d.ts +4 -0
  92. package/dist/syntax/index.js +4 -0
  93. package/docs/compat.md +76 -27
  94. package/docs/custom-rule-tutorial.md +9 -3
  95. package/docs/examples/no-listed-commands-rule.ts +2 -2
  96. package/docs/rules/README.md +36 -4
  97. package/docs/rules/_template.md +2 -2
  98. package/docs/rules/compat/no-append-all-redirect.md +12 -12
  99. package/docs/rules/compat/no-associative-arrays.md +14 -14
  100. package/docs/rules/compat/no-brace-sequence-padding-step.md +12 -12
  101. package/docs/rules/compat/no-case-attributes.md +13 -13
  102. package/docs/rules/compat/no-case-fallthrough.md +12 -12
  103. package/docs/rules/compat/no-case-modification.md +11 -11
  104. package/docs/rules/compat/no-coproc.md +13 -13
  105. package/docs/rules/compat/no-declare-g.md +13 -13
  106. package/docs/rules/compat/no-double-bracket.md +2 -2
  107. package/docs/rules/compat/no-epoch-variables.md +13 -13
  108. package/docs/rules/compat/no-fd-variable.md +12 -12
  109. package/docs/rules/compat/no-globstar.md +13 -13
  110. package/docs/rules/compat/no-lastpipe-semantics.md +13 -13
  111. package/docs/rules/compat/no-let.md +2 -2
  112. package/docs/rules/compat/no-mapfile.md +16 -16
  113. package/docs/rules/compat/no-nameref.md +13 -13
  114. package/docs/rules/compat/no-negative-array-index.md +14 -14
  115. package/docs/rules/compat/no-nounset-empty-array.md +72 -17
  116. package/docs/rules/compat/no-parameter-transformation.md +19 -19
  117. package/docs/rules/compat/no-pipe-all.md +13 -13
  118. package/docs/rules/compat/no-printf-time-format.md +13 -13
  119. package/docs/rules/compat/no-source.md +2 -2
  120. package/docs/rules/compat/no-test-v.md +12 -12
  121. package/docs/rules/compat/no-unicode-escape.md +13 -13
  122. package/docs/rules/compat/no-wait-n.md +13 -13
  123. package/docs/rules/no-dynamic-invocation.md +17 -12
  124. package/docs/rules/no-eval.md +6 -1
  125. package/docs/rules/no-masked-exit-status.md +4 -0
  126. package/docs/rules/no-secret-env-reads.md +34 -3
  127. package/docs/rules/no-secret-flags.md +19 -2
  128. package/docs/rules/no-unchecked-cd.md +9 -2
  129. package/docs/rules/no-undeclared-commands.md +16 -15
  130. package/docs/rules/no-unused-declarations.md +17 -20
  131. package/docs/rules/prefer-local.md +20 -2
  132. package/docs/rules/quote-expansions.md +8 -5
  133. package/docs/rules/require-network-wrapper.md +22 -17
  134. package/docs/rules/require-shebang.md +10 -4
  135. package/docs/rules/require-strict-mode.md +6 -0
  136. package/docs/shellcheck.md +9 -5
  137. package/docs/transparent-wrappers.md +97 -0
  138. package/docs/wrapper-rules.md +9 -0
  139. package/package.json +2 -2
  140. package/dist/language/end-of-input-marker.d.ts +0 -29
  141. package/dist/language/end-of-input-marker.js +0 -88
@@ -3,7 +3,7 @@
3
3
  export type ShellDialect = 'bash' | 'posix' | 'mksh' | 'bats' | 'zsh';
4
4
  /**
5
5
  * `languageOptions` accepted by {@link ShellLanguage}: `dialect` selects
6
- * mvdan/sh's `LangVariant` through a consumer-owned mapping (default `"bash"`); `shellVersion`
6
+ * mvdan/sh's `LangVariant` through a consumer-owned mapping (default `"bash"`); `minBashVersion`
7
7
  * is consumed by the rule layer (e.g. the bash-3.2 compatibility pack),
8
8
  * never by the parser itself.
9
9
  *
@@ -19,13 +19,14 @@ export type ShellDialect = 'bash' | 'posix' | 'mksh' | 'bats' | 'zsh';
19
19
  export interface ShellLanguageOptions extends Record<string, unknown> {
20
20
  readonly dialect?: ShellDialect;
21
21
  /**
22
- * The Bash version (`MAJOR[.MINOR]`, such as `"3.2"`) the scripts must run
23
- * on. The parser ignores it. The `compat/*` rules read it: unset means the
24
- * newest version, versions compare numerically by major then minor, and
25
- * under `dialect: "posix"` Bash-only features are unavailable at any
26
- * version.
22
+ * The minimum Bash version (`MAJOR[.MINOR]`, such as `"3.2"`) the
23
+ * script must support, including every newer Bash. Only valid with the
24
+ * `bash` or `bats` dialect (the default is `bash`). The parser ignores
25
+ * it; `compat/*` rules report features unavailable at this floor. Unset
26
+ * means no Bash version constraint. Versions compare numerically by
27
+ * major then minor, with no upper version limit.
27
28
  */
28
- readonly shellVersion?: string;
29
+ readonly minBashVersion?: string;
29
30
  /**
30
31
  * The prefix directive/inline-config comments must start with — e.g. the
31
32
  * default `"eslint"` recognizes `# eslint-disable-next-line ...` and
@@ -38,14 +39,14 @@ export interface ShellLanguageOptions extends Record<string, unknown> {
38
39
  }
39
40
  /**
40
41
  * The plain-data shape {@link NormalizedShellLanguageOptions.toJSON} returns:
41
- * `dialect` and `directivePrefix` always present (both defaulted), `shellVersion`
42
+ * `dialect` and `directivePrefix` always present (both defaulted), `minBashVersion`
42
43
  * present only when the caller supplied one. Keys are always emitted in this
43
44
  * fixed order so that two logically-identical inputs serialize identically
44
45
  * regardless of the key order the caller happened to use — the property this
45
46
  * type exists to guarantee for cache-key stability (see
46
47
  * {@link NormalizedShellLanguageOptions}). `directivePrefix` is included
47
- * unconditionally (like `dialect`, unlike `shellVersion`) because — like
48
- * `dialect` and unlike `shellVersion` — it always has a concrete effect on
48
+ * unconditionally (like `dialect`, unlike `minBashVersion`) because — like
49
+ * `dialect` and unlike `minBashVersion` — it always has a concrete effect on
49
50
  * linting results (which comments the `SourceCode` recognizes as directives)
50
51
  * whether or not the caller supplied one, so two configs that differ only in
51
52
  * an *implicit* default vs. an *explicit* `directivePrefix: "eslint"` must
@@ -56,7 +57,7 @@ export interface ShellLanguageOptions extends Record<string, unknown> {
56
57
  export interface ShellLanguageOptionsJSON {
57
58
  readonly dialect: ShellDialect;
58
59
  readonly directivePrefix: string;
59
- readonly shellVersion?: string;
60
+ readonly minBashVersion?: string;
60
61
  }
61
62
  /**
62
63
  * The result of {@link ShellLanguage.normalizeLanguageOptions}: `dialect` and
@@ -73,19 +74,21 @@ export interface ShellLanguageOptionsJSON {
73
74
  export interface NormalizedShellLanguageOptions extends Record<string, unknown> {
74
75
  readonly dialect: ShellDialect;
75
76
  readonly directivePrefix: string;
76
- readonly shellVersion?: string;
77
+ readonly minBashVersion?: string;
77
78
  toJSON(): ShellLanguageOptionsJSON;
78
79
  }
79
80
  /**
80
81
  * Validates `languageOptions` for {@link ShellLanguage}. Throws a
81
82
  * `TypeError` when `dialect` is present but not one of the five supported
82
- * dialects, when `shellVersion` is present but not a string or does not
83
- * match the `MAJOR[.MINOR]` numeric shape (see {@link SHELL_VERSION_PATTERN};
83
+ * dialects, when `minBashVersion` is present but not a string or does not
84
+ * match the `MAJOR[.MINOR]` numeric shape (see {@link MIN_BASH_VERSION_PATTERN};
84
85
  * e.g. `''` and `'not-a-version'` are rejected, but a shape-valid,
85
86
  * nonexistent version like `'99.99'` is accepted — shape is validated, not
86
87
  * membership in a known-versions list), or when `directivePrefix` is present
87
88
  * but not a valid directive-comment label (see {@link DIRECTIVE_PREFIX_PATTERN}).
88
- * Unknown keys are silently ignored, per the ESLint `Language` contract.
89
+ * The Bash floor is rejected for dialects other than `bash` and `bats`.
90
+ * The removed `shellVersion` key throws with migration guidance, even if
91
+ * its value is undefined. Other unknown keys are silently ignored.
89
92
  *
90
93
  * The parameter is treated as `Record<string, unknown>` internally (rather
91
94
  * than trusting {@link ShellLanguageOptions}'s declared field types) because
@@ -30,31 +30,27 @@ function isValidDirectivePrefix(value) {
30
30
  return typeof value === 'string' && DIRECTIVE_PREFIX_PATTERN.test(value);
31
31
  }
32
32
  /**
33
- * Matches the `MAJOR[.MINOR]` numeric shape {@link validateShellLanguageOptions}
34
- * requires `shellVersion` to have (issue #34: the sibling `dialect` option throws
35
- * an exemplary enum-listing TypeError, while `shellVersion` accepted any
36
- * string — including `''` and `'not-a-version'` — with zero feedback). This
37
- * validates *shape only*, not membership in a known-versions list: a
38
- * shape-valid but nonexistent version like `'99.99'` is intentionally
39
- * accepted, per this issue's acceptance criteria and non-goals (per-dialect
40
- * known-version allowlists are out of scope). How rules consume
41
- * `shellVersion` is defined by the `compat/*` rules' gate
42
- * (`src/rules/compat/version-gates.ts`, documented in `docs/compat.md`).
33
+ * The Bash floor accepts numeric MAJOR[.MINOR] values, including future
34
+ * releases. Validation checks shape rather than a release allowlist;
35
+ * compatibility rules compare the floor with each feature's first release.
43
36
  */
44
- const SHELL_VERSION_PATTERN = /^\d+(?:\.\d+)?$/u;
45
- function isValidShellVersionShape(value) {
46
- return SHELL_VERSION_PATTERN.test(value);
37
+ const MIN_BASH_VERSION_PATTERN = /^\d+(?:\.\d+)?$/u;
38
+ /** Whether a Bash floor has the numeric shape required for comparison. */
39
+ function isValidMinBashVersionShape(value) {
40
+ return MIN_BASH_VERSION_PATTERN.test(value);
47
41
  }
48
42
  /**
49
43
  * Validates `languageOptions` for {@link ShellLanguage}. Throws a
50
44
  * `TypeError` when `dialect` is present but not one of the five supported
51
- * dialects, when `shellVersion` is present but not a string or does not
52
- * match the `MAJOR[.MINOR]` numeric shape (see {@link SHELL_VERSION_PATTERN};
45
+ * dialects, when `minBashVersion` is present but not a string or does not
46
+ * match the `MAJOR[.MINOR]` numeric shape (see {@link MIN_BASH_VERSION_PATTERN};
53
47
  * e.g. `''` and `'not-a-version'` are rejected, but a shape-valid,
54
48
  * nonexistent version like `'99.99'` is accepted — shape is validated, not
55
49
  * membership in a known-versions list), or when `directivePrefix` is present
56
50
  * but not a valid directive-comment label (see {@link DIRECTIVE_PREFIX_PATTERN}).
57
- * Unknown keys are silently ignored, per the ESLint `Language` contract.
51
+ * The Bash floor is rejected for dialects other than `bash` and `bats`.
52
+ * The removed `shellVersion` key throws with migration guidance, even if
53
+ * its value is undefined. Other unknown keys are silently ignored.
58
54
  *
59
55
  * The parameter is treated as `Record<string, unknown>` internally (rather
60
56
  * than trusting {@link ShellLanguageOptions}'s declared field types) because
@@ -64,15 +60,26 @@ function isValidShellVersionShape(value) {
64
60
  * @public
65
61
  */
66
62
  export function validateShellLanguageOptions(languageOptions) {
67
- const { dialect, shellVersion, directivePrefix } = languageOptions;
63
+ // A removed key must not disappear during normalization: that would
64
+ // silently disable the consumer's compatibility requirement.
65
+ if ('shellVersion' in languageOptions) {
66
+ throw new TypeError('@eslint-sh/eslint-plugin: languageOptions.shellVersion has been removed. Use languageOptions.minBashVersion with dialect "bash" or "bats" instead.');
67
+ }
68
+ const { dialect, minBashVersion, directivePrefix } = languageOptions;
68
69
  if (dialect !== undefined && !isShellDialect(dialect)) {
69
70
  throw new TypeError(`@eslint-sh/eslint-plugin: languageOptions.dialect must be one of ${SUPPORTED_SHELL_DIALECTS.join(', ')}; received ${JSON.stringify(dialect)}.`);
70
71
  }
71
- if (shellVersion !== undefined && typeof shellVersion !== 'string') {
72
- throw new TypeError(`@eslint-sh/eslint-plugin: languageOptions.shellVersion must be a string (e.g. "3.2"); received ${JSON.stringify(shellVersion)}.`);
72
+ if (minBashVersion !== undefined &&
73
+ dialect !== undefined &&
74
+ dialect !== 'bash' &&
75
+ dialect !== 'bats') {
76
+ throw new TypeError(`@eslint-sh/eslint-plugin: languageOptions.minBashVersion applies only to bash and bats; dialect is "${dialect}". Remove minBashVersion or set dialect to "bash" or "bats".`);
77
+ }
78
+ if (minBashVersion !== undefined && typeof minBashVersion !== 'string') {
79
+ throw new TypeError(`@eslint-sh/eslint-plugin: languageOptions.minBashVersion must be a string (e.g. "3.2"); received ${JSON.stringify(minBashVersion)}.`);
73
80
  }
74
- if (typeof shellVersion === 'string' && !isValidShellVersionShape(shellVersion)) {
75
- throw new TypeError(`@eslint-sh/eslint-plugin: languageOptions.shellVersion must match the shape MAJOR[.MINOR] (e.g. "3.2", "5"); received ${JSON.stringify(shellVersion)}.`);
81
+ if (typeof minBashVersion === 'string' && !isValidMinBashVersionShape(minBashVersion)) {
82
+ throw new TypeError(`@eslint-sh/eslint-plugin: languageOptions.minBashVersion must match the shape MAJOR[.MINOR] (e.g. "3.2", "5"); received ${JSON.stringify(minBashVersion)}.`);
76
83
  }
77
84
  if (directivePrefix !== undefined && !isValidDirectivePrefix(directivePrefix)) {
78
85
  throw new TypeError(`@eslint-sh/eslint-plugin: languageOptions.directivePrefix must be one or more lowercase, hyphen-separated words (e.g. "eslint", "shellcheck-sh"); received ${JSON.stringify(directivePrefix)}.`);
@@ -91,18 +98,18 @@ export function validateShellLanguageOptions(languageOptions) {
91
98
  export function normalizeShellLanguageOptions(languageOptions) {
92
99
  const dialect = languageOptions.dialect ?? 'bash';
93
100
  const directivePrefix = languageOptions.directivePrefix ?? DEFAULT_DIRECTIVE_PREFIX;
94
- const shellVersion = languageOptions.shellVersion;
101
+ const minBashVersion = languageOptions.minBashVersion;
95
102
  return {
96
103
  dialect,
97
104
  directivePrefix,
98
105
  // `exactOptionalPropertyTypes` forbids assigning `undefined` to an
99
106
  // optional property directly — omit the key entirely rather than set it
100
- // to `undefined` when the caller didn't supply a shellVersion.
101
- ...(shellVersion === undefined ? {} : { shellVersion }),
107
+ // to `undefined` when the caller didn't supply a minBashVersion.
108
+ ...(minBashVersion === undefined ? {} : { minBashVersion }),
102
109
  toJSON() {
103
- return shellVersion === undefined
110
+ return minBashVersion === undefined
104
111
  ? { dialect, directivePrefix }
105
- : { dialect, directivePrefix, shellVersion };
112
+ : { dialect, directivePrefix, minBashVersion };
106
113
  },
107
114
  };
108
115
  }
@@ -26,11 +26,12 @@ export declare class ShellLanguage implements Language {
26
26
  readonly defaultLanguageOptions: ShellLanguageOptions;
27
27
  /**
28
28
  * Validates `languageOptions.dialect` (must be one of the five supported
29
- * dialects, if present), `languageOptions.shellVersion` (must be a
30
- * string matching the `MAJOR[.MINOR]` numeric shape, if present), and
29
+ * dialects, if present), `languageOptions.minBashVersion` (must be a
30
+ * string matching the `MAJOR[.MINOR]` numeric shape, if present, and
31
+ * applies only to `bash` and `bats`), and
31
32
  * `languageOptions.directivePrefix` (must be a valid directive-comment
32
- * label, if present). Unknown keys are silently ignored, per the
33
- * `Language` contract. See `shell-language-options.ts`'s
33
+ * label, if present). The removed `shellVersion` key throws with
34
+ * migration guidance; other unknown keys are silently ignored. See `shell-language-options.ts`'s
34
35
  * `validateShellLanguageOptions` for the full rationale.
35
36
  */
36
37
  validateLanguageOptions(languageOptions: ShellLanguageOptions): void;
@@ -64,9 +65,9 @@ export declare class ShellLanguage implements Language {
64
65
  * A file whose extension marks it as a known non-shell source type (for
65
66
  * example `.js` or `.json`) is never shell-parsed: it fails with a single
66
67
  * diagnostic explaining that a `language: 'sh/sh'` config entry without
67
- * `files` reached it, and how to scope that entry. A source in which
68
- * the parser silently drops text at a U+0080 fails at that character; a
69
- * U+0080 the parser reads as an ordinary character does not.
68
+ * `files` reached it, and how to scope that entry. Unicode characters remain
69
+ * source text. A native nesting-budget refusal returns no partial tree and
70
+ * becomes a fatal resource diagnostic at 1:1.
70
71
  */
71
72
  parse(file: File, context: {
72
73
  languageOptions: ShellLanguageOptions;
@@ -81,8 +82,8 @@ export declare class ShellLanguage implements Language {
81
82
  * present, but `?? 'eslint'` stays as an explicit, honest fallback for
82
83
  * direct construction (e.g. tests that skip the `Language` normalization
83
84
  * step), mirroring `parse()`'s `dialect` default above. `dialect` and
84
- * `shellVersion` are forwarded so rules read the same normalized values
85
- * through `sourceCode.dialect` / `sourceCode.shellVersion`.
85
+ * `minBashVersion` are forwarded so rules read the same normalized values
86
+ * through `sourceCode.dialect` / `sourceCode.minBashVersion`.
86
87
  */
87
88
  createSourceCode(file: File, parseResult: OkParseResult<ShellFile>, context: {
88
89
  languageOptions: ShellLanguageOptions;
@@ -1,6 +1,5 @@
1
1
  /** Adapts upstream parsing and diagnostics to ESLint's synchronous Language contract. */
2
- import { LangBash, LangBats, LangError, LangMirBSDKorn, LangPOSIX, LangZsh, ParseError, parseWithPositionsSync, } from 'sh-ast';
3
- import { firstDroppedTextIndex } from './end-of-input-marker.js';
2
+ import { LangBash, LangBats, LangError, LangMirBSDKorn, LangPOSIX, LangZsh, ParseError, ShParseMaxDepthError, parseWithPositionsSync, } from 'sh-ast';
4
3
  import { nonShellFileError } from './non-shell-file-guard.js';
5
4
  import { matchesSelectorClass as matchesShellSelectorClass } from './selector-classes.js';
6
5
  import { ShellSourceCode } from './shell-source-code.js';
@@ -50,28 +49,15 @@ function toFileError(error, source) {
50
49
  column: location.column,
51
50
  };
52
51
  }
52
+ // A native budget refusal carries no partial tree or source position. It is
53
+ // an input resource limit, not a broken parser runtime; later files may parse.
54
+ if (error instanceof ShParseMaxDepthError) {
55
+ return { message: error.message, line: 1, column: 1 };
56
+ }
53
57
  // Bridge/input failures carry no asserted syntax position. The fallback is an ESLint presentation policy.
54
58
  const detail = error instanceof Error ? error.message : String(error);
55
59
  return { message: `${PARSER_RUNTIME_FAILURE_PREFIX}${detail}`, line: 1, column: 1 };
56
60
  }
57
- /**
58
- * The fatal parse error for a source in which the parser drops text at a U+0080, at that
59
- * character, or `undefined` when nothing is dropped. Checked before parsing so a silently
60
- * truncated tree never reaches the rules. A U+0080 the parser reads as an ordinary character
61
- * (inside a quoted-delimiter heredoc body, after a backslash) is not an error; one the parser
62
- * rejects (inside quotes or an expansion) is reported by the parser itself.
63
- */
64
- function endOfInputMarkerError(source, options) {
65
- const index = firstDroppedTextIndex(source, options);
66
- if (index === undefined)
67
- return undefined;
68
- const location = new SourceCoordinates(source).atIndex(index);
69
- return {
70
- message: 'the character U+0080 is not supported: the parser reads it as end of input',
71
- line: location.line,
72
- column: location.column,
73
- };
74
- }
75
61
  /**
76
62
  * `ShellLanguage` — the `@eslint/core` `Language` implementation for shell
77
63
  * scripts. Registered by the plugin as `languages.sh`, so consumers select
@@ -96,11 +82,12 @@ export class ShellLanguage {
96
82
  defaultLanguageOptions = { dialect: 'bash' };
97
83
  /**
98
84
  * Validates `languageOptions.dialect` (must be one of the five supported
99
- * dialects, if present), `languageOptions.shellVersion` (must be a
100
- * string matching the `MAJOR[.MINOR]` numeric shape, if present), and
85
+ * dialects, if present), `languageOptions.minBashVersion` (must be a
86
+ * string matching the `MAJOR[.MINOR]` numeric shape, if present, and
87
+ * applies only to `bash` and `bats`), and
101
88
  * `languageOptions.directivePrefix` (must be a valid directive-comment
102
- * label, if present). Unknown keys are silently ignored, per the
103
- * `Language` contract. See `shell-language-options.ts`'s
89
+ * label, if present). The removed `shellVersion` key throws with
90
+ * migration guidance; other unknown keys are silently ignored. See `shell-language-options.ts`'s
104
91
  * `validateShellLanguageOptions` for the full rationale.
105
92
  */
106
93
  validateLanguageOptions(languageOptions) {
@@ -140,9 +127,9 @@ export class ShellLanguage {
140
127
  * A file whose extension marks it as a known non-shell source type (for
141
128
  * example `.js` or `.json`) is never shell-parsed: it fails with a single
142
129
  * diagnostic explaining that a `language: 'sh/sh'` config entry without
143
- * `files` reached it, and how to scope that entry. A source in which
144
- * the parser silently drops text at a U+0080 fails at that character; a
145
- * U+0080 the parser reads as an ordinary character does not.
130
+ * `files` reached it, and how to scope that entry. Unicode characters remain
131
+ * source text. A native nesting-budget refusal returns no partial tree and
132
+ * becomes a fatal resource diagnostic at 1:1.
146
133
  */
147
134
  parse(file, context) {
148
135
  const misappliedError = nonShellFileError(file.path);
@@ -156,10 +143,6 @@ export class ShellLanguage {
156
143
  Filename: file.path,
157
144
  KeepComments: true,
158
145
  };
159
- const unsupported = endOfInputMarkerError(text, parseOptions);
160
- if (unsupported !== undefined) {
161
- return { ok: false, errors: [unsupported] };
162
- }
163
146
  try {
164
147
  const parsed = parseWithPositionsSync(text, parseOptions);
165
148
  const ast = adaptSyntaxFile(parsed.File, text, parsed.Positions);
@@ -179,8 +162,8 @@ export class ShellLanguage {
179
162
  * present, but `?? 'eslint'` stays as an explicit, honest fallback for
180
163
  * direct construction (e.g. tests that skip the `Language` normalization
181
164
  * step), mirroring `parse()`'s `dialect` default above. `dialect` and
182
- * `shellVersion` are forwarded so rules read the same normalized values
183
- * through `sourceCode.dialect` / `sourceCode.shellVersion`.
165
+ * `minBashVersion` are forwarded so rules read the same normalized values
166
+ * through `sourceCode.dialect` / `sourceCode.minBashVersion`.
184
167
  */
185
168
  createSourceCode(file, parseResult, context) {
186
169
  return new ShellSourceCode({
@@ -188,9 +171,9 @@ export class ShellLanguage {
188
171
  ast: parseResult.ast,
189
172
  directivePrefix: context.languageOptions.directivePrefix ?? 'eslint',
190
173
  dialect: context.languageOptions.dialect ?? 'bash',
191
- ...(context.languageOptions.shellVersion === undefined
174
+ ...(context.languageOptions.minBashVersion === undefined
192
175
  ? {}
193
- : { shellVersion: context.languageOptions.shellVersion }),
176
+ : { minBashVersion: context.languageOptions.minBashVersion }),
194
177
  });
195
178
  }
196
179
  }
@@ -37,7 +37,7 @@ import type { ShellCommandSite } from './command-resolution.js';
37
37
  * the string `jq`?", "which command does this invocation run?", and "which
38
38
  * commands does this file run, and where?" by delegating to
39
39
  * `sh-ast/analyze`. They never reinterpret words themselves, so every rule
40
- * gets the same answer for the same syntax. `dialect` and `shellVersion`
40
+ * gets the same answer for the same syntax. `dialect` and `minBashVersion`
41
41
  * expose the file's normalized language options.
42
42
  *
43
43
  * Syntax access (`getSyntax`, `getViewNode`, `getIndexFromSyntaxPos`,
@@ -64,7 +64,7 @@ export declare class ShellSourceCode extends TextSourceCodeBase<{
64
64
  ast: ShFile;
65
65
  directivePrefix?: string;
66
66
  dialect?: ShellDialect;
67
- shellVersion?: string;
67
+ minBashVersion?: string;
68
68
  });
69
69
  /**
70
70
  * The shell dialect this file was parsed as: the normalized
@@ -73,11 +73,12 @@ export declare class ShellSourceCode extends TextSourceCodeBase<{
73
73
  */
74
74
  get dialect(): ShellDialect;
75
75
  /**
76
- * The configured `languageOptions.shellVersion` (a `MAJOR[.MINOR]` string
76
+ * The configured `languageOptions.minBashVersion` (a `MAJOR[.MINOR]` string
77
77
  * such as `"3.2"`), or `undefined` when the config does not set one. The
78
- * parser never reads it; it exists for rules that gate on shell version.
78
+ * script must support this Bash and every newer release. The parser never
79
+ * reads it; compatibility rules use it as a floor with no upper limit.
79
80
  */
80
- get shellVersion(): string | undefined;
81
+ get minBashVersion(): string | undefined;
81
82
  /**
82
83
  * The one-based location of a UTF-16 `index` in this file's text, where
83
84
  * `index` may be anywhere from 0 to `text.length`. CRLF counts as one line
@@ -166,13 +167,19 @@ export declare class ShellSourceCode extends TextSourceCodeBase<{
166
167
  * effective command's name as
167
168
  * `res.effective.static ? res.effective.text : null`.
168
169
  * `sudo -u x "$prog"` and `exec "$@"` have a non-static `effective` with
169
- * reason `expansion`; `env -S '…'` has reason `embedded-command`; `chain`
170
+ * reason `expansion`; `env -S '…'` and `bash -c '…'` have reason
171
+ * `embedded-command`; `chain`
170
172
  * lists every word resolved along the way, outermost wrapper first, and
171
173
  * `chainIndices` gives each entry's position in the command's `argv` (as
172
174
  * {@link ShellSourceCode.getCommandSites} reports it). When `effective` is
173
175
  * static, the effective command's arguments are
174
176
  * `argv.slice(chainIndices.at(-1)! + 1)`: every entry after the effective
175
- * command's own index.
177
+ * command's own index. For an interpreter command string the final chain
178
+ * index instead locates the selector flag; the interpreter is in `chain.at(-2)`.
179
+ * `embeddedCommandInterpreters: []` retains the interpreter as the static
180
+ * effective command when a policy needs program identity. Utility wrappers
181
+ * can match absolute-path basenames; `shellBuiltin: true` entries require
182
+ * exact names, so external paths do not acquire in-process semantics.
176
183
  *
177
184
  * Enumeration runs once per file, the first time any command helper is
178
185
  * called. If the file's nesting exceeds `enumerateCommands`'s recursion
@@ -333,8 +340,9 @@ export declare class ShellSourceCode extends TextSourceCodeBase<{
333
340
  * @throws TypeError when `node` has no original record in this file (see
334
341
  * {@link ShellSourceCode.getEditableSyntax}), when `edited` is not a
335
342
  * printable root (the message names its type), or when the printer's output
336
- * is not valid UTF-8. Non-UTF-8 output comes back from `printSync` as a
337
- * `Uint8Array` and is never decoded lossily into a fix.
343
+ * is not valid UTF-8, or the edited syntax contains literal vertical tabs or
344
+ * form feeds that the printer cannot preserve. Non-UTF-8 output comes back
345
+ * from `printSync` as a `Uint8Array` and is never decoded lossily into a fix.
338
346
  * @throws `PrintError` or `PrintPanicError` (from sh-ast) when upstream's
339
347
  * printer fails for `edited` or `options`.
340
348
  */
@@ -5,6 +5,38 @@ import { SYNTAX_INTERFACES, printSync } from 'sh-ast';
5
5
  import { resolveArgv0 } from 'sh-ast/analyze';
6
6
  import { buildCommandSiteIndex, rawCallExprFor, viewCommandSites } from './command-resolution.js';
7
7
  import { resolveWord } from './word-resolution.js';
8
+ /**
9
+ * Refuses edited syntax whose string or byte values the upstream printer treats as layout.
10
+ * Checking the edited tree also catches controls introduced by a rule, without refusing ordinary
11
+ * text edits elsewhere in a file. The input filename is metadata, never printer output.
12
+ * Iteration tolerates shared/cyclic records; sh-ast still validates
13
+ * the printable syntax itself.
14
+ */
15
+ // format.ts's refusePrinterControlCharacters independently guards the formatter's source text.
16
+ function refusePrinterControlCharacters(root) {
17
+ const pending = [root.Type === 'File' ? { ...root, Name: '' } : root];
18
+ const seen = new Set();
19
+ while (pending.length > 0) {
20
+ const current = pending.pop();
21
+ const unsafe = (typeof current === 'string' && /[\v\f]/u.test(current)) ||
22
+ (current instanceof Uint8Array && current.some((byte) => byte === 0x0b || byte === 0x0c));
23
+ if (unsafe) {
24
+ throw new TypeError('cannot print a text fix containing literal vertical tabs and form feeds: ' +
25
+ 'the printer changes their value');
26
+ }
27
+ // A checked byte-valued string is a leaf; its individual numbers are not syntax children.
28
+ if (current instanceof Uint8Array) {
29
+ continue;
30
+ }
31
+ if (current !== null && typeof current === 'object' && !seen.has(current)) {
32
+ seen.add(current);
33
+ const children = Object.values(current);
34
+ for (const child of children) {
35
+ pending.push(child);
36
+ }
37
+ }
38
+ }
39
+ }
8
40
  /**
9
41
  * Shared instance — `ConfigCommentParser` is stateless (see
10
42
  * `@eslint/plugin-kit`'s implementation), so every `ShellSourceCode`
@@ -296,7 +328,7 @@ function withUnusedDirectiveFixRange(comment, text) {
296
328
  * the string `jq`?", "which command does this invocation run?", and "which
297
329
  * commands does this file run, and where?" by delegating to
298
330
  * `sh-ast/analyze`. They never reinterpret words themselves, so every rule
299
- * gets the same answer for the same syntax. `dialect` and `shellVersion`
331
+ * gets the same answer for the same syntax. `dialect` and `minBashVersion`
300
332
  * expose the file's normalized language options.
301
333
  *
302
334
  * Syntax access (`getSyntax`, `getViewNode`, `getIndexFromSyntaxPos`,
@@ -356,8 +388,8 @@ export class ShellSourceCode extends TextSourceCodeBase {
356
388
  #inlineConfigNodes;
357
389
  /** The normalized `languageOptions.dialect` this file was parsed with. */
358
390
  #dialect;
359
- /** The configured `languageOptions.shellVersion`, if the config supplied one. */
360
- #shellVersion;
391
+ /** The configured `languageOptions.minBashVersion`, if the config supplied one. */
392
+ #minBashVersion;
361
393
  /** This file's coordinate index; see `#textCoordinates`. */
362
394
  #ownCoordinates;
363
395
  /**
@@ -380,7 +412,7 @@ export class ShellSourceCode extends TextSourceCodeBase {
380
412
  // supplies the already-normalized, defaulted values.
381
413
  this.#directivePrefix = options.directivePrefix ?? 'eslint';
382
414
  this.#dialect = options.dialect ?? 'bash';
383
- this.#shellVersion = options.shellVersion;
415
+ this.#minBashVersion = options.minBashVersion;
384
416
  this.#directiveLabels = buildDirectiveLabels(this.#directivePrefix);
385
417
  this.#inlineConfigPattern = buildInlineConfigPattern(this.#directivePrefix);
386
418
  }
@@ -393,12 +425,13 @@ export class ShellSourceCode extends TextSourceCodeBase {
393
425
  return this.#dialect;
394
426
  }
395
427
  /**
396
- * The configured `languageOptions.shellVersion` (a `MAJOR[.MINOR]` string
428
+ * The configured `languageOptions.minBashVersion` (a `MAJOR[.MINOR]` string
397
429
  * 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.
430
+ * script must support this Bash and every newer release. The parser never
431
+ * reads it; compatibility rules use it as a floor with no upper limit.
399
432
  */
400
- get shellVersion() {
401
- return this.#shellVersion;
433
+ get minBashVersion() {
434
+ return this.#minBashVersion;
402
435
  }
403
436
  /**
404
437
  * The one-based location of a UTF-16 `index` in this file's text, where
@@ -521,13 +554,19 @@ export class ShellSourceCode extends TextSourceCodeBase {
521
554
  * effective command's name as
522
555
  * `res.effective.static ? res.effective.text : null`.
523
556
  * `sudo -u x "$prog"` and `exec "$@"` have a non-static `effective` with
524
- * reason `expansion`; `env -S '…'` has reason `embedded-command`; `chain`
557
+ * reason `expansion`; `env -S '…'` and `bash -c '…'` have reason
558
+ * `embedded-command`; `chain`
525
559
  * lists every word resolved along the way, outermost wrapper first, and
526
560
  * `chainIndices` gives each entry's position in the command's `argv` (as
527
561
  * {@link ShellSourceCode.getCommandSites} reports it). When `effective` is
528
562
  * static, the effective command's arguments are
529
563
  * `argv.slice(chainIndices.at(-1)! + 1)`: every entry after the effective
530
- * command's own index.
564
+ * command's own index. For an interpreter command string the final chain
565
+ * index instead locates the selector flag; the interpreter is in `chain.at(-2)`.
566
+ * `embeddedCommandInterpreters: []` retains the interpreter as the static
567
+ * effective command when a policy needs program identity. Utility wrappers
568
+ * can match absolute-path basenames; `shellBuiltin: true` entries require
569
+ * exact names, so external paths do not acquire in-process semantics.
531
570
  *
532
571
  * Enumeration runs once per file, the first time any command helper is
533
572
  * called. If the file's nesting exceeds `enumerateCommands`'s recursion
@@ -702,8 +741,9 @@ export class ShellSourceCode extends TextSourceCodeBase {
702
741
  * @throws TypeError when `node` has no original record in this file (see
703
742
  * {@link ShellSourceCode.getEditableSyntax}), when `edited` is not a
704
743
  * 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.
744
+ * is not valid UTF-8, or the edited syntax contains literal vertical tabs or
745
+ * form feeds that the printer cannot preserve. Non-UTF-8 output comes back
746
+ * from `printSync` as a `Uint8Array` and is never decoded lossily into a fix.
707
747
  * @throws `PrintError` or `PrintPanicError` (from sh-ast) when upstream's
708
748
  * printer fails for `edited` or `options`.
709
749
  */
@@ -711,6 +751,7 @@ export class ShellSourceCode extends TextSourceCodeBase {
711
751
  const original = this.#originalSyntaxFor(node);
712
752
  const rootType = printableRootType(edited);
713
753
  const root = edited.Type === 'Stmt' ? { ...edited, Comments: null } : edited;
754
+ refusePrinterControlCharacters(root);
714
755
  const printed = printSync(root, options);
715
756
  if (typeof printed !== 'string') {
716
757
  throw new TypeError('printSync produced output that is not valid UTF-8; a text fix cannot hold it without ' +
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The common Bash 3.2+ intrinsic numeric profile used by shell rules. Value
3
+ * shape and assignment behavior are separate properties: a variable that
4
+ * always yields an integer need not evaluate assigned text arithmetically.
5
+ * Writable intrinsic setters still differ from ordinary string bindings;
6
+ * their handling can vary between Bash versions.
7
+ *
8
+ * This is a name profile, not dataflow over unset, local, or declaration
9
+ * attributes. Callers decide when intrinsic meaning applies. POSIX and other
10
+ * shell dialects do not inherit this Bash profile, and newer Bash-only names
11
+ * are excluded from the common minimum-version guarantee.
12
+ */
13
+ /** A name whose intrinsic value is numeric throughout the common Bash profile. @internal */
14
+ export declare function isBashIntegerVariable(name: string): boolean;
15
+ /**
16
+ * A name with a non-ordinary writable integer setter. OPTIND can evaluate
17
+ * assigned text as arithmetic; numeric output alone makes no such promise
18
+ * about the other setters. Proofs that do not model these setters must fence
19
+ * their writes rather than treat them as ordinary string assignments.
20
+ * @internal
21
+ */
22
+ export declare function hasSpecialBashIntegerSetter(name: string): boolean;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The common Bash 3.2+ intrinsic numeric profile used by shell rules. Value
3
+ * shape and assignment behavior are separate properties: a variable that
4
+ * always yields an integer need not evaluate assigned text arithmetically.
5
+ * Writable intrinsic setters still differ from ordinary string bindings;
6
+ * their handling can vary between Bash versions.
7
+ *
8
+ * This is a name profile, not dataflow over unset, local, or declaration
9
+ * attributes. Callers decide when intrinsic meaning applies. POSIX and other
10
+ * shell dialects do not inherit this Bash profile, and newer Bash-only names
11
+ * are excluded from the common minimum-version guarantee.
12
+ */
13
+ /**
14
+ * Every listed intrinsic yields a decimal integer. Writable setters control
15
+ * the random generator, elapsed clock, or option parser; the other bindings
16
+ * ignore assignments or reject them. Environment-controlled names such as
17
+ * UID/EUID/COLUMNS, and later additions such as BASHPID/SRANDOM, are outside
18
+ * this common profile.
19
+ */
20
+ const INTEGER_BINDINGS = new Map([
21
+ ['RANDOM', 'writable'],
22
+ ['SECONDS', 'writable'],
23
+ ['OPTIND', 'writable'],
24
+ ['LINENO', 'ignored'],
25
+ ['BASH_SUBSHELL', 'ignored'],
26
+ ['PPID', 'readonly'],
27
+ ]);
28
+ /** A name whose intrinsic value is numeric throughout the common Bash profile. @internal */
29
+ export function isBashIntegerVariable(name) {
30
+ return INTEGER_BINDINGS.has(name);
31
+ }
32
+ /**
33
+ * A name with a non-ordinary writable integer setter. OPTIND can evaluate
34
+ * assigned text as arithmetic; numeric output alone makes no such promise
35
+ * about the other setters. Proofs that do not model these setters must fence
36
+ * their writes rather than treat them as ordinary string assignments.
37
+ * @internal
38
+ */
39
+ export function hasSpecialBashIntegerSetter(name) {
40
+ return INTEGER_BINDINGS.get(name) === 'writable';
41
+ }
@@ -4,7 +4,7 @@
4
4
  * A compat rule reports one gated feature (a row of the gate table in
5
5
  * `./version-gates.ts`) wherever a file uses it while its `languageOptions`
6
6
  * say the feature is unavailable. The factory owns the gate decision, so no
7
- * compat rule reads `shellVersion` or `dialect` itself:
7
+ * compat rule reads `minBashVersion` or `dialect` itself:
8
8
  *
9
9
  * - when {@link isFeatureAvailable} says the feature is available, the rule
10
10
  * returns no listeners at all, so it costs nothing and cannot report;
@@ -1,4 +1,4 @@
1
- import type { ShellSourceCode } from '../../language/shell-source-code.js';
1
+ import type { TransparentWrappersBindingContext } from '../resolve-transparent-wrappers.js';
2
2
  import type { ShellNode } from '../../language/syntax-view.js';
3
3
  import type { ShellRuleVisitor } from '../define-shell-rule.js';
4
4
  /**
@@ -16,4 +16,4 @@ export declare const DECLARATION_AND_LOCAL_COMMANDS: ReadonlySet<string>;
16
16
  *
17
17
  * @internal
18
18
  */
19
- export declare function declarationFlagVisitor(sourceCode: ShellSourceCode, flag: string, report: (option: ShellNode, command: string, options: readonly string[]) => void, commands?: ReadonlySet<string>): ShellRuleVisitor;
19
+ export declare function declarationFlagVisitor(context: TransparentWrappersBindingContext, flag: string, report: (option: ShellNode, command: string, options: readonly string[]) => void, commands?: ReadonlySet<string>): ShellRuleVisitor;