@eslint-sh/eslint-plugin 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +223 -17
  2. package/dist/command-harvest/builtins/bash.d.ts +36 -0
  3. package/dist/command-harvest/builtins/bash.js +98 -0
  4. package/dist/command-harvest/builtins/bats.d.ts +45 -0
  5. package/dist/command-harvest/builtins/bats.js +33 -0
  6. package/dist/command-harvest/builtins/index.d.ts +18 -0
  7. package/dist/command-harvest/builtins/index.js +39 -0
  8. package/dist/command-harvest/builtins/posix.d.ts +51 -0
  9. package/dist/command-harvest/builtins/posix.js +93 -0
  10. package/dist/command-harvest/harvest-commands.d.ts +66 -0
  11. package/dist/command-harvest/harvest-commands.js +97 -0
  12. package/dist/command-harvest/wrapper-table.d.ts +24 -0
  13. package/dist/command-harvest/wrapper-table.js +49 -0
  14. package/dist/configs/agent-authored.d.ts +41 -0
  15. package/dist/configs/agent-authored.js +33 -0
  16. package/dist/configs/bash-3-2.d.ts +28 -0
  17. package/dist/configs/bash-3-2.js +50 -0
  18. package/dist/configs/posix-portable.d.ts +40 -0
  19. package/dist/configs/posix-portable.js +52 -0
  20. package/dist/configs/recommended.d.ts +52 -0
  21. package/dist/configs/recommended.js +34 -0
  22. package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
  23. package/dist/declaration-conventions/comment-pragma.js +153 -0
  24. package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
  25. package/dist/declaration-conventions/declaration-convention.js +1 -0
  26. package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
  27. package/dist/declaration-conventions/read-declared-commands.js +128 -0
  28. package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
  29. package/dist/eslint-plugin-syntax.d.ts +27 -0
  30. package/dist/eslint-plugin.d.ts +1211 -52
  31. package/dist/index.d.ts +56 -8
  32. package/dist/index.js +53 -6
  33. package/dist/language/command-resolution.d.ts +124 -0
  34. package/dist/language/command-resolution.js +120 -0
  35. package/dist/language/end-of-input-marker.d.ts +29 -0
  36. package/dist/language/end-of-input-marker.js +88 -0
  37. package/dist/language/line-breaks.d.ts +18 -0
  38. package/dist/language/line-breaks.js +22 -0
  39. package/dist/language/lint-view-types.d.ts +104 -0
  40. package/dist/language/lint-view-types.js +1 -0
  41. package/dist/language/non-shell-file-guard.d.ts +35 -0
  42. package/dist/language/non-shell-file-guard.js +59 -0
  43. package/dist/language/selector-classes.d.ts +0 -1
  44. package/dist/language/selector-classes.js +0 -1
  45. package/dist/language/shell-language-options.d.ts +7 -1
  46. package/dist/language/shell-language-options.js +4 -4
  47. package/dist/language/shell-language.d.ts +10 -2
  48. package/dist/language/shell-language.js +59 -8
  49. package/dist/language/shell-source-code.d.ts +342 -5
  50. package/dist/language/shell-source-code.js +559 -30
  51. package/dist/language/source-coordinates.d.ts +13 -2
  52. package/dist/language/source-coordinates.js +68 -14
  53. package/dist/language/syntax-view.d.ts +60 -39
  54. package/dist/language/syntax-view.js +200 -45
  55. package/dist/language/word-resolution.d.ts +4 -3
  56. package/dist/language/word-resolution.js +4 -3
  57. package/dist/rules/compat/create-compat-rule.d.ts +59 -0
  58. package/dist/rules/compat/create-compat-rule.js +65 -0
  59. package/dist/rules/compat/declaration-options.d.ts +19 -0
  60. package/dist/rules/compat/declaration-options.js +110 -0
  61. package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
  62. package/dist/rules/compat/no-append-all-redirect.js +73 -0
  63. package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
  64. package/dist/rules/compat/no-associative-arrays.js +39 -0
  65. package/dist/rules/compat/no-case-attributes.d.ts +12 -0
  66. package/dist/rules/compat/no-case-attributes.js +63 -0
  67. package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
  68. package/dist/rules/compat/no-case-fallthrough.js +67 -0
  69. package/dist/rules/compat/no-case-modification.d.ts +35 -0
  70. package/dist/rules/compat/no-case-modification.js +86 -0
  71. package/dist/rules/compat/no-coproc.d.ts +12 -0
  72. package/dist/rules/compat/no-coproc.js +47 -0
  73. package/dist/rules/compat/no-declare-g.d.ts +12 -0
  74. package/dist/rules/compat/no-declare-g.js +59 -0
  75. package/dist/rules/compat/no-double-bracket.d.ts +12 -0
  76. package/dist/rules/compat/no-double-bracket.js +52 -0
  77. package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
  78. package/dist/rules/compat/no-epoch-variables.js +57 -0
  79. package/dist/rules/compat/no-fd-variable.d.ts +12 -0
  80. package/dist/rules/compat/no-fd-variable.js +48 -0
  81. package/dist/rules/compat/no-globstar.d.ts +12 -0
  82. package/dist/rules/compat/no-globstar.js +39 -0
  83. package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
  84. package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
  85. package/dist/rules/compat/no-let.d.ts +12 -0
  86. package/dist/rules/compat/no-let.js +62 -0
  87. package/dist/rules/compat/no-mapfile.d.ts +13 -0
  88. package/dist/rules/compat/no-mapfile.js +122 -0
  89. package/dist/rules/compat/no-nameref.d.ts +12 -0
  90. package/dist/rules/compat/no-nameref.js +44 -0
  91. package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
  92. package/dist/rules/compat/no-negative-array-index.js +123 -0
  93. package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
  94. package/dist/rules/compat/no-nounset-empty-array.js +158 -0
  95. package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
  96. package/dist/rules/compat/no-parameter-transformation.js +123 -0
  97. package/dist/rules/compat/no-pipe-all.d.ts +13 -0
  98. package/dist/rules/compat/no-pipe-all.js +68 -0
  99. package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
  100. package/dist/rules/compat/no-printf-time-format.js +99 -0
  101. package/dist/rules/compat/no-source.d.ts +13 -0
  102. package/dist/rules/compat/no-source.js +103 -0
  103. package/dist/rules/compat/no-test-v.d.ts +12 -0
  104. package/dist/rules/compat/no-test-v.js +280 -0
  105. package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
  106. package/dist/rules/compat/no-unicode-escape.js +120 -0
  107. package/dist/rules/compat/no-wait-n.d.ts +12 -0
  108. package/dist/rules/compat/no-wait-n.js +73 -0
  109. package/dist/rules/compat/operator-range.d.ts +10 -0
  110. package/dist/rules/compat/operator-range.js +15 -0
  111. package/dist/rules/compat/posix-absent-command.d.ts +12 -0
  112. package/dist/rules/compat/posix-absent-command.js +134 -0
  113. package/dist/rules/compat/shopt-options.d.ts +9 -0
  114. package/dist/rules/compat/shopt-options.js +63 -0
  115. package/dist/rules/compat/version-gates.d.ts +76 -0
  116. package/dist/rules/compat/version-gates.js +197 -0
  117. package/dist/rules/create-rule.d.ts +91 -0
  118. package/dist/rules/create-rule.js +51 -0
  119. package/dist/rules/create-wrapper-rule.d.ts +166 -0
  120. package/dist/rules/create-wrapper-rule.js +407 -0
  121. package/dist/rules/define-shell-rule.d.ts +184 -0
  122. package/dist/rules/define-shell-rule.js +56 -0
  123. package/dist/rules/index.d.ts +26 -0
  124. package/dist/rules/index.js +88 -0
  125. package/dist/rules/max-script-complexity.d.ts +28 -0
  126. package/dist/rules/max-script-complexity.js +130 -0
  127. package/dist/rules/no-dynamic-invocation.d.ts +35 -0
  128. package/dist/rules/no-dynamic-invocation.js +117 -0
  129. package/dist/rules/no-eval.d.ts +13 -0
  130. package/dist/rules/no-eval.js +89 -0
  131. package/dist/rules/no-masked-exit-status.d.ts +21 -0
  132. package/dist/rules/no-masked-exit-status.js +667 -0
  133. package/dist/rules/no-secret-env-reads.d.ts +24 -0
  134. package/dist/rules/no-secret-env-reads.js +133 -0
  135. package/dist/rules/no-secret-flags.d.ts +32 -0
  136. package/dist/rules/no-secret-flags.js +162 -0
  137. package/dist/rules/no-unchecked-cd.d.ts +44 -0
  138. package/dist/rules/no-unchecked-cd.js +250 -0
  139. package/dist/rules/no-undeclared-commands.d.ts +20 -0
  140. package/dist/rules/no-undeclared-commands.js +115 -0
  141. package/dist/rules/no-unused-declarations.d.ts +23 -0
  142. package/dist/rules/no-unused-declarations.js +259 -0
  143. package/dist/rules/prefer-local.d.ts +16 -0
  144. package/dist/rules/prefer-local.js +249 -0
  145. package/dist/rules/quote-expansions.d.ts +13 -0
  146. package/dist/rules/quote-expansions.js +229 -0
  147. package/dist/rules/require-network-wrapper.d.ts +40 -0
  148. package/dist/rules/require-network-wrapper.js +244 -0
  149. package/dist/rules/require-shebang.d.ts +29 -0
  150. package/dist/rules/require-shebang.js +101 -0
  151. package/dist/rules/require-strict-mode.d.ts +52 -0
  152. package/dist/rules/require-strict-mode.js +177 -0
  153. package/dist/rules/require-tty-guard.d.ts +13 -0
  154. package/dist/rules/require-tty-guard.js +289 -0
  155. package/dist/rules/resolve-declaration-convention.d.ts +96 -0
  156. package/dist/rules/resolve-declaration-convention.js +109 -0
  157. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  158. package/dist/rules/resolve-network-wrapper.js +116 -0
  159. package/dist/rules/set-options.d.ts +109 -0
  160. package/dist/rules/set-options.js +201 -0
  161. package/dist/rules/shebang-interpreter.d.ts +18 -0
  162. package/dist/rules/shebang-interpreter.js +81 -0
  163. package/dist/rules/shebang-matches-dialect.d.ts +12 -0
  164. package/dist/rules/shebang-matches-dialect.js +69 -0
  165. package/dist/rules/transparent-wrappers.d.ts +52 -0
  166. package/dist/rules/transparent-wrappers.js +99 -0
  167. package/dist/rules/user-pattern.d.ts +63 -0
  168. package/dist/rules/user-pattern.js +184 -0
  169. package/dist/syntax/analyze.d.ts +19 -0
  170. package/dist/syntax/analyze.js +21 -0
  171. package/dist/syntax/index.d.ts +23 -0
  172. package/dist/syntax/index.js +25 -0
  173. package/dist/version.d.ts +0 -1
  174. package/dist/version.js +0 -1
  175. package/docs/compat.md +347 -0
  176. package/docs/custom-rule-tutorial.md +780 -34
  177. package/docs/declaration-conventions.md +478 -0
  178. package/docs/examples/audit-wrapper-pack.mjs +15 -0
  179. package/docs/examples/dep-line-convention.mjs +22 -0
  180. package/docs/examples/eslint.config.mjs +3 -2
  181. package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
  182. package/docs/examples/no-backticks-rule-tester.mjs +106 -0
  183. package/docs/examples/no-backticks-rule.mjs +60 -0
  184. package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
  185. package/docs/examples/no-function-keyword-rule.mjs +59 -0
  186. package/docs/examples/no-listed-commands-rule.ts +63 -0
  187. package/docs/examples/no-rm-rf-rule.mjs +26 -15
  188. package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
  189. package/docs/examples/sidecar-convention.mjs +38 -0
  190. package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
  191. package/docs/examples/sorted-declarations-rule.mjs +52 -0
  192. package/docs/reading-nodes.md +159 -0
  193. package/docs/rules/README.md +78 -0
  194. package/docs/rules/_template.md +83 -0
  195. package/docs/rules/compat/no-append-all-redirect.md +87 -0
  196. package/docs/rules/compat/no-associative-arrays.md +95 -0
  197. package/docs/rules/compat/no-case-attributes.md +82 -0
  198. package/docs/rules/compat/no-case-fallthrough.md +90 -0
  199. package/docs/rules/compat/no-case-modification.md +74 -0
  200. package/docs/rules/compat/no-coproc.md +76 -0
  201. package/docs/rules/compat/no-declare-g.md +80 -0
  202. package/docs/rules/compat/no-double-bracket.md +72 -0
  203. package/docs/rules/compat/no-epoch-variables.md +79 -0
  204. package/docs/rules/compat/no-fd-variable.md +78 -0
  205. package/docs/rules/compat/no-globstar.md +74 -0
  206. package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
  207. package/docs/rules/compat/no-let.md +78 -0
  208. package/docs/rules/compat/no-mapfile.md +118 -0
  209. package/docs/rules/compat/no-nameref.md +81 -0
  210. package/docs/rules/compat/no-negative-array-index.md +87 -0
  211. package/docs/rules/compat/no-nounset-empty-array.md +103 -0
  212. package/docs/rules/compat/no-parameter-transformation.md +94 -0
  213. package/docs/rules/compat/no-pipe-all.md +85 -0
  214. package/docs/rules/compat/no-printf-time-format.md +75 -0
  215. package/docs/rules/compat/no-source.md +98 -0
  216. package/docs/rules/compat/no-test-v.md +82 -0
  217. package/docs/rules/compat/no-unicode-escape.md +91 -0
  218. package/docs/rules/compat/no-wait-n.md +85 -0
  219. package/docs/rules/max-script-complexity.md +109 -0
  220. package/docs/rules/no-dynamic-invocation.md +139 -0
  221. package/docs/rules/no-eval.md +106 -0
  222. package/docs/rules/no-masked-exit-status.md +239 -0
  223. package/docs/rules/no-secret-env-reads.md +173 -0
  224. package/docs/rules/no-secret-flags.md +169 -0
  225. package/docs/rules/no-unchecked-cd.md +146 -0
  226. package/docs/rules/no-undeclared-commands.md +141 -0
  227. package/docs/rules/no-unused-declarations.md +216 -0
  228. package/docs/rules/prefer-local.md +221 -0
  229. package/docs/rules/quote-expansions.md +157 -0
  230. package/docs/rules/require-network-wrapper.md +171 -0
  231. package/docs/rules/require-shebang.md +157 -0
  232. package/docs/rules/require-strict-mode.md +253 -0
  233. package/docs/rules/require-tty-guard.md +128 -0
  234. package/docs/rules/shebang-matches-dialect.md +120 -0
  235. package/docs/selectors.md +2 -2
  236. package/docs/shellcheck.md +86 -0
  237. package/docs/wrapper-rules.md +206 -0
  238. package/package.json +26 -20
  239. package/dist/index.d.ts.map +0 -1
  240. package/dist/index.js.map +0 -1
  241. package/dist/language/selector-classes.d.ts.map +0 -1
  242. package/dist/language/selector-classes.js.map +0 -1
  243. package/dist/language/shell-language-options.d.ts.map +0 -1
  244. package/dist/language/shell-language-options.js.map +0 -1
  245. package/dist/language/shell-language.d.ts.map +0 -1
  246. package/dist/language/shell-language.js.map +0 -1
  247. package/dist/language/shell-source-code.d.ts.map +0 -1
  248. package/dist/language/shell-source-code.js.map +0 -1
  249. package/dist/language/source-coordinates.d.ts.map +0 -1
  250. package/dist/language/source-coordinates.js.map +0 -1
  251. package/dist/language/syntax-view.d.ts.map +0 -1
  252. package/dist/language/syntax-view.js.map +0 -1
  253. package/dist/language/word-resolution.d.ts.map +0 -1
  254. package/dist/language/word-resolution.js.map +0 -1
  255. package/dist/version.d.ts.map +0 -1
  256. package/dist/version.js.map +0 -1
@@ -0,0 +1,68 @@
1
+ /**
2
+ * `sh/compat/no-pipe-all`: reports the `|&` pipe, shorthand for `2>&1 |`, in
3
+ * files whose target shell lacks it, that is, Bash older than 4.0 (macOS's
4
+ * stock `/bin/bash` is 3.2) and POSIX sh.
5
+ *
6
+ * Bash 3.2 rejects `|&` as a syntax error, so the whole script fails to run.
7
+ * The portable spelling is `cmd 2>&1 | other`. The Bash manual defines `|&`
8
+ * as that implicit `2>&1`, applied after any redirections the left-hand
9
+ * command writes, so writing `2>&1` just before the `|` is exactly
10
+ * equivalent. The rule offers that rewrite as a suggestion.
11
+ *
12
+ * What is read: a pipeline (`BinaryCmd`) whose operator is `|&`. A plain `|`,
13
+ * `||`, `&&`, a background `&`, and any of these inside quotes are other
14
+ * syntax and are not reported. The report is on the `|&` operator.
15
+ */
16
+ import { PipeAll } from 'sh-ast';
17
+ import { createCompatRule } from './create-compat-rule.js';
18
+ import { operatorRange } from './operator-range.js';
19
+ /**
20
+ * The `sh/compat/no-pipe-all` rule module.
21
+ *
22
+ * @internal
23
+ */
24
+ export const noPipeAll = createCompatRule({
25
+ name: 'compat/no-pipe-all',
26
+ feature: 'pipeAll',
27
+ meta: {
28
+ type: 'problem',
29
+ hasSuggestions: true,
30
+ docs: {
31
+ description: 'Disallow the `|&` pipe when the target shell lacks it',
32
+ },
33
+ messages: {
34
+ pipeAll: '`|&` needs {{required}}, but this file targets {{target}}. Write `cmd 2>&1 | other` instead.',
35
+ useStderrRedirect: 'Replace `|&` with `2>&1 |`.',
36
+ },
37
+ },
38
+ create(context, gate) {
39
+ const { sourceCode } = context;
40
+ return {
41
+ BinaryCmd(node) {
42
+ if (node.op !== PipeAll)
43
+ return;
44
+ const range = operatorRange(sourceCode, node, '|&');
45
+ if (range === undefined) {
46
+ context.report({ node, messageId: 'pipeAll', data: { ...gate } });
47
+ return;
48
+ }
49
+ const [start, end] = range;
50
+ // `cmd|&other` has no space before the operator, and `2>&1` must not
51
+ // join the command's last word.
52
+ const before = sourceCode.text[start - 1];
53
+ const separator = before === ' ' || before === '\t' ? '' : ' ';
54
+ context.report({
55
+ loc: { start: sourceCode.getLocFromIndex(start), end: sourceCode.getLocFromIndex(end) },
56
+ messageId: 'pipeAll',
57
+ data: { ...gate },
58
+ suggest: [
59
+ {
60
+ messageId: 'useStderrRedirect',
61
+ fix: (fixer) => fixer.replaceTextRange(range, `${separator}2>&1 |`),
62
+ },
63
+ ],
64
+ });
65
+ },
66
+ };
67
+ },
68
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoPrintfTimeFormatMessageId = 'printfTime';
7
+ /**
8
+ * The `sh/compat/no-printf-time-format` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noPrintfTimeFormat: import("../define-shell-rule.js").ShellRule<[], "printfTime">;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `sh/compat/no-printf-time-format`: reports `printf`'s `%(fmt)T` date
3
+ * format in files whose target shell lacks it, that is, Bash older than 4.2
4
+ * (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
5
+ *
6
+ * Bash 4.1 rejects the directive with "invalid format character", so
7
+ * nothing is printed. The message suggests the portable alternative for the
8
+ * current time: `date '+fmt'`, which takes the same `strftime` format.
9
+ *
10
+ * What is read: a `printf` command (after transparent wrappers such as
11
+ * `builtin`) whose format argument, after an optional `-v name` and `--`, is
12
+ * a static word containing a `%(…)T` directive. The format is scanned as
13
+ * `printf` reads it: `%%` is a literal percent sign, and a directive may
14
+ * carry flags, a width and a precision before the `(`. A format that is not
15
+ * static cannot be read, and is not reported. The report is on the format
16
+ * word.
17
+ */
18
+ import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
19
+ import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
20
+ import { createCompatRule } from './create-compat-rule.js';
21
+ /**
22
+ * The wrappers followed to find the `printf` command: `sh-ast/analyze`'s
23
+ * defaults (which include `command`) plus Bash's `builtin`.
24
+ */
25
+ const TRANSPARENT_WRAPPERS = [
26
+ ...DEFAULT_TRANSPARENT_WRAPPERS,
27
+ BUILTIN_WRAPPER,
28
+ ];
29
+ /** A conversion's flags, width and precision, as `printf` accepts them. */
30
+ const DIRECTIVE_PREFIX = /^[-+ #0']*(?:\d+|\*)?(?:\.(?:\d+|\*)?)?/u;
31
+ /**
32
+ * The `strftime` format of the first `%(fmt)T` directive in a `printf`
33
+ * format string, or `undefined` when it has none.
34
+ */
35
+ function timeFormatIn(format) {
36
+ for (let index = 0; index < format.length; index++) {
37
+ if (format[index] !== '%')
38
+ continue;
39
+ if (format[index + 1] === '%') {
40
+ index++;
41
+ continue;
42
+ }
43
+ const rest = format.slice(index + 1);
44
+ const prefix = DIRECTIVE_PREFIX.exec(rest)?.[0] ?? '';
45
+ const afterPrefix = rest.slice(prefix.length);
46
+ if (!afterPrefix.startsWith('('))
47
+ continue;
48
+ const close = afterPrefix.indexOf(')');
49
+ if (close !== -1 && afterPrefix[close + 1] === 'T')
50
+ return afterPrefix.slice(1, close);
51
+ }
52
+ return undefined;
53
+ }
54
+ /**
55
+ * The `sh/compat/no-printf-time-format` rule module.
56
+ *
57
+ * @internal
58
+ */
59
+ export const noPrintfTimeFormat = createCompatRule({
60
+ name: 'compat/no-printf-time-format',
61
+ feature: 'printfTimeFormat',
62
+ meta: {
63
+ type: 'problem',
64
+ docs: {
65
+ description: "Disallow `printf`'s `%(fmt)T` date format when the target shell lacks it",
66
+ },
67
+ messages: {
68
+ printfTime: "`printf '%({{format}})T'` needs {{required}}, but this file targets {{target}}. For the current time, run `date '+{{format}}'` instead.",
69
+ },
70
+ },
71
+ create(context, gate) {
72
+ const { sourceCode } = context;
73
+ return {
74
+ CallExpr(node) {
75
+ const invoked = sourceCode.resolveInvokedCommand(node, {
76
+ transparentWrappers: TRANSPARENT_WRAPPERS,
77
+ });
78
+ if (invoked?.effective.static !== true || invoked.effective.text !== 'printf')
79
+ return;
80
+ // `chainIndices` index `node.args` directly, so slice before filtering.
81
+ const words = node.args.slice((invoked.chainIndices.at(-1) ?? 0) + 1);
82
+ let index = 0;
83
+ const first = words.at(0);
84
+ if (first !== undefined && sourceCode.resolveWordText(first) === '-v')
85
+ index = 2;
86
+ if (index < words.length && sourceCode.resolveWordText(words[index]) === '--')
87
+ index++;
88
+ const formatWord = words.at(index);
89
+ if (formatWord === undefined)
90
+ return;
91
+ const text = sourceCode.resolveWordText(formatWord);
92
+ const format = text === null ? undefined : timeFormatIn(text);
93
+ if (format === undefined)
94
+ return;
95
+ context.report({ node: formatWord, messageId: 'printfTime', data: { ...gate, format } });
96
+ },
97
+ };
98
+ },
99
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Message ids this rule reports with: `source` for the report and `useDot` for
3
+ * its suggestion.
4
+ *
5
+ * @internal
6
+ */
7
+ export type NoSourceMessageId = 'source' | 'useDot';
8
+ /**
9
+ * The `sh/compat/no-source` rule module.
10
+ *
11
+ * @internal
12
+ */
13
+ export declare const noSource: import("../define-shell-rule.js").ShellRule<[], NoSourceMessageId>;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * `sh/compat/no-source`: reports the `source` builtin in files that target
3
+ * POSIX sh, where the portable name is the `.` special built-in. Every Bash has
4
+ * `source`, so this is a POSIX-only gate.
5
+ *
6
+ * POSIX section 2.9.1.4 leaves the results of a command named `source`
7
+ * unspecified. dash has no such command: `source ./lib.sh` fails with `source:
8
+ * not found` and status 127, and the file is never read. `bash --posix` and
9
+ * BusyBox ash accept it, so only some POSIX shells show the problem.
10
+ *
11
+ * What is read: a command whose effective name is `source`, outside a branch
12
+ * for one shell (see `./posix-absent-command.ts`).
13
+ *
14
+ * A function named `source` anywhere in the file exempts a call that names it
15
+ * directly, since dash can then run that function; `bash --posix` refuses that
16
+ * definition, which this rule does not judge. This is a name check across the
17
+ * file and does not prove the function exists at the call: execution order, a
18
+ * definition in a branch or subshell, and `unset -f` are not read, so some exempt
19
+ * calls still fail on dash. A call through a wrapper is never exempt, because
20
+ * wrappers such as `command`, `env` and `exec` do not run functions.
21
+ *
22
+ * The suggestion replaces the word with `.` only where the two read the same
23
+ * file with the same effect, apart from a file that cannot be read: `.` then
24
+ * ends the script on a POSIX shell, so a `|| fallback` never runs, and the
25
+ * suggestion's text says so. The conditions are:
26
+ *
27
+ * - The command is written directly, with no wrapper.
28
+ * - It has no leading `VAR=value`. Such an assignment persists after `.` on a
29
+ * POSIX shell, while Bash outside POSIX mode scopes it to the `source`
30
+ * command.
31
+ * - It has exactly one operand. `.` takes one in POSIX, and dash ignores any
32
+ * others where `source` would set the positional parameters.
33
+ * - The operand has a literal `/`. A name without one makes `.` search `PATH`.
34
+ */
35
+ import { definedFunctionNames } from '../../command-harvest/harvest-commands.js';
36
+ import { childNodes } from '../../language/syntax-view.js';
37
+ import { createCompatRule } from './create-compat-rule.js';
38
+ import { reachableCommandWord } from './posix-absent-command.js';
39
+ /** The builtin this rule gates. */
40
+ const SOURCE = 'source';
41
+ /**
42
+ * Whether `word` holds a `/` that no expansion can remove: in unquoted or
43
+ * quoted literal text, whatever its other parts expand to. A `/` inside an
44
+ * expansion (`${x%/*}`, `$(pwd)`) does not count, so the walk stops at anything
45
+ * that is not literal text or a double-quoted group.
46
+ */
47
+ function hasLiteralSlash(word) {
48
+ return childNodes(word).some((part) => {
49
+ if (part.type === 'Lit' || part.type === 'SglQuoted')
50
+ return part.value.includes('/');
51
+ return part.type === 'DblQuoted' && hasLiteralSlash(part);
52
+ });
53
+ }
54
+ /** Whether `.` is an exact replacement for this `source` command (see the module comment). */
55
+ function dotIsEquivalent(sourceCode, node) {
56
+ if (sourceCode.getCommandName(node) !== SOURCE)
57
+ return false;
58
+ if (node.assigns.length > 0 || node.args.length !== 2)
59
+ return false;
60
+ return hasLiteralSlash(node.args[1]);
61
+ }
62
+ /**
63
+ * The `sh/compat/no-source` rule module.
64
+ *
65
+ * @internal
66
+ */
67
+ export const noSource = createCompatRule({
68
+ name: 'compat/no-source',
69
+ feature: 'sourceCommand',
70
+ meta: {
71
+ type: 'problem',
72
+ hasSuggestions: true,
73
+ docs: {
74
+ description: 'Disallow the `source` builtin when the target shell is POSIX sh',
75
+ },
76
+ messages: {
77
+ source: '`source` is not part of {{target}}. dash fails with `source: not found` and the file is never read. Use the `.` special built-in with a path that contains a slash, for example `. ./lib.sh`.',
78
+ useDot: 'Replace `source` with `.`, which ends the script if the file is missing.',
79
+ },
80
+ },
81
+ create(context, gate) {
82
+ const { sourceCode } = context;
83
+ const fileDefinesSource = definedFunctionNames(sourceCode).has(SOURCE);
84
+ return {
85
+ CallExpr(node) {
86
+ const word = reachableCommandWord(sourceCode, node, SOURCE);
87
+ if (word === undefined)
88
+ return;
89
+ // Only a name written directly can run a function: a wrapper skips it.
90
+ if (fileDefinesSource && sourceCode.getCommandName(node) === SOURCE)
91
+ return;
92
+ context.report({
93
+ node: word,
94
+ messageId: 'source',
95
+ data: { target: gate.target },
96
+ suggest: dotIsEquivalent(sourceCode, node)
97
+ ? [{ messageId: 'useDot', fix: (fixer) => fixer.replaceText(word, '.') }]
98
+ : [],
99
+ });
100
+ },
101
+ };
102
+ },
103
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Message ids this rule reports with.
3
+ *
4
+ * @internal
5
+ */
6
+ export type NoTestVMessageId = 'testV';
7
+ /**
8
+ * The `sh/compat/no-test-v` rule module.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const noTestV: import("../define-shell-rule.js").ShellRule<[], "testV">;
@@ -0,0 +1,280 @@
1
+ /**
2
+ * `sh/compat/no-test-v`: reports the `-v name` test (`[[ -v x ]]`,
3
+ * `test -v x`, `[ -v x ]`) in files whose target shell lacks it, that is,
4
+ * Bash older than 4.2 (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
5
+ *
6
+ * In Bash 4.1 and older, `[[ -v x ]]` is a syntax error, so the whole script
7
+ * fails to parse, and `test -v x` fails with "unary operator expected". The
8
+ * message suggests the portable alternative, `[ -n "${x+set}" ]`, which is
9
+ * true exactly when `x` is set (even to the empty string) and is safe under
10
+ * `set -u`.
11
+ *
12
+ * What is read:
13
+ *
14
+ * - inside `[[ … ]]`, the parser's `-v` unary test node;
15
+ * - for `test` and `[` (after transparent wrappers such as `builtin`), the
16
+ * arguments, read the way Bash's `test` reads them: with two arguments a
17
+ * leading `-v` is the operator; with three, the middle one is a binary
18
+ * operator if it can be one, and `! -v name` negates the test; with four,
19
+ * `! …` and `( … )` peel off; anything longer goes through `test`'s own
20
+ * precedence parser, where `-v` is the operator only where a primary is
21
+ * expected and another argument follows it. So `[ "$x" = -v ]` and
22
+ * `[ -n -v ]` do not report. A double-quoted expansion (`"$name"`) is one
23
+ * operand whatever its value. Any other argument that is not a static word
24
+ * ends the reading, because it may expand to several arguments or none.
25
+ *
26
+ * The report is on the `[[` test, or on the `-v` word.
27
+ */
28
+ import { TsVarSet } from 'sh-ast';
29
+ import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
30
+ import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
31
+ import { createCompatRule } from './create-compat-rule.js';
32
+ /**
33
+ * The wrappers followed to find the `test` command: `sh-ast/analyze`'s
34
+ * defaults (which include `command`) plus Bash's `builtin`.
35
+ */
36
+ const TRANSPARENT_WRAPPERS = [
37
+ ...DEFAULT_TRANSPARENT_WRAPPERS,
38
+ BUILTIN_WRAPPER,
39
+ ];
40
+ /** The commands that are Bash's `test` builtin. */
41
+ const TEST_COMMANDS = new Set(['test', '[']);
42
+ /** Bash's `test` binary operators (`test_binop`), without `-a` and `-o`. */
43
+ const BINARY_OPERATORS = new Set([
44
+ '=',
45
+ '==',
46
+ '!=',
47
+ '<',
48
+ '>',
49
+ '-eq',
50
+ '-ne',
51
+ '-lt',
52
+ '-le',
53
+ '-gt',
54
+ '-ge',
55
+ '-nt',
56
+ '-ot',
57
+ '-ef',
58
+ ]);
59
+ /** Bash's `test` unary operators (`test_unop`). */
60
+ const UNARY_OPERATORS = new Set([
61
+ '-a',
62
+ '-b',
63
+ '-c',
64
+ '-d',
65
+ '-e',
66
+ '-f',
67
+ '-g',
68
+ '-h',
69
+ '-k',
70
+ '-n',
71
+ '-o',
72
+ '-p',
73
+ '-r',
74
+ '-s',
75
+ '-t',
76
+ '-u',
77
+ '-v',
78
+ '-w',
79
+ '-x',
80
+ '-z',
81
+ '-G',
82
+ '-L',
83
+ '-O',
84
+ '-S',
85
+ '-N',
86
+ '-R',
87
+ ]);
88
+ /**
89
+ * What a double-quoted, non-static argument stands for while reading `test`'s
90
+ * arguments: an operand that is never an operator. A NUL cannot occur in a
91
+ * shell word, so no real argument equals it.
92
+ */
93
+ const QUOTED_OPERAND = '\0operand';
94
+ /** A shell variable name, optionally with an array subscript. */
95
+ const VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(?:\[[^\]]*\])?$/u;
96
+ /**
97
+ * The indices of the arguments in `args` that Bash's `test` reads as the
98
+ * `-v` operator. `args` are the static texts after the command name (and
99
+ * without a closing `]`); reading stops at the first `null`.
100
+ */
101
+ function variableTestIndices(args) {
102
+ const end = args.indexOf(null);
103
+ const argv = (end === -1 ? args : args.slice(0, end));
104
+ const found = [];
105
+ /** `test`'s two-argument rule, at offset `at`. */
106
+ function twoArguments(at) {
107
+ if (argv[at] === '-v')
108
+ found.push(at);
109
+ }
110
+ /** `test`'s three-argument rule, at offset `at`. */
111
+ function threeArguments(at) {
112
+ const middle = argv[at + 1];
113
+ if (BINARY_OPERATORS.has(middle) || middle === '-a' || middle === '-o')
114
+ return true;
115
+ if (argv[at] === '!') {
116
+ twoArguments(at + 1);
117
+ return true;
118
+ }
119
+ return argv[at] === '(' && argv[at + 2] === ')';
120
+ }
121
+ /** `test`'s precedence parser over every argument (`expr` in Bash's test.c). */
122
+ function expression() {
123
+ let pos = 0;
124
+ const term = () => {
125
+ if (pos >= argv.length)
126
+ return;
127
+ const word = argv[pos];
128
+ if (word === '!') {
129
+ pos++;
130
+ term();
131
+ return;
132
+ }
133
+ if (word === '(') {
134
+ pos++;
135
+ or();
136
+ if (argv[pos] === ')')
137
+ pos++;
138
+ return;
139
+ }
140
+ if (pos + 3 <= argv.length && BINARY_OPERATORS.has(argv[pos + 1])) {
141
+ pos += 3;
142
+ return;
143
+ }
144
+ if (UNARY_OPERATORS.has(word) && pos + 1 < argv.length) {
145
+ if (word === '-v')
146
+ found.push(pos);
147
+ pos += 2;
148
+ return;
149
+ }
150
+ pos++;
151
+ };
152
+ const and = () => {
153
+ term();
154
+ while (argv[pos] === '-a') {
155
+ pos++;
156
+ term();
157
+ }
158
+ };
159
+ const or = () => {
160
+ and();
161
+ while (argv[pos] === '-o') {
162
+ pos++;
163
+ and();
164
+ }
165
+ };
166
+ while (pos < argv.length) {
167
+ const before = pos;
168
+ or();
169
+ if (pos === before)
170
+ pos++;
171
+ }
172
+ }
173
+ switch (argv.length) {
174
+ case 0:
175
+ case 1:
176
+ break;
177
+ case 2:
178
+ if (argv[0] !== '!')
179
+ twoArguments(0);
180
+ break;
181
+ case 3:
182
+ if (!threeArguments(0))
183
+ expression();
184
+ break;
185
+ case 4:
186
+ if (argv[0] === '!') {
187
+ if (!threeArguments(1))
188
+ expression();
189
+ }
190
+ else if (argv[0] === '(' && argv[3] === ')') {
191
+ if (argv[1] !== '!')
192
+ twoArguments(1);
193
+ }
194
+ else {
195
+ expression();
196
+ }
197
+ break;
198
+ default:
199
+ expression();
200
+ }
201
+ return found;
202
+ }
203
+ /**
204
+ * The `sh/compat/no-test-v` rule module.
205
+ *
206
+ * @internal
207
+ */
208
+ export const noTestV = createCompatRule({
209
+ name: 'compat/no-test-v',
210
+ feature: 'testVariableSet',
211
+ meta: {
212
+ type: 'problem',
213
+ docs: {
214
+ description: 'Disallow the `-v name` test (`[[ -v x ]]`, `test -v x`) when the target shell lacks it',
215
+ },
216
+ messages: {
217
+ testV: '`-v {{name}}` needs {{required}}, but this file targets {{target}}. Test whether the variable is set with `[ -n "${{{name}}+set}" ]` instead.',
218
+ },
219
+ },
220
+ create(context, gate) {
221
+ const { sourceCode } = context;
222
+ /** The variable name shown in the message for `operand`. */
223
+ function nameOf(operand) {
224
+ const text = operand?.type === 'Word' ? sourceCode.resolveWordText(operand) : null;
225
+ return text !== null && VARIABLE_NAME.test(text) ? text : 'name';
226
+ }
227
+ /**
228
+ * The argument `word` gives `test`: its static text; a stand-in operand
229
+ * for a word that is one double-quoted string, which always makes exactly
230
+ * one argument whatever it expands to; or `null` for any other word, which
231
+ * may split into several arguments or none.
232
+ */
233
+ function argumentText(word) {
234
+ const text = sourceCode.resolveWordText(word);
235
+ if (text !== null)
236
+ return text;
237
+ if (word.parts.length === 1 && word.parts[0].type === 'DblQuoted') {
238
+ return QUOTED_OPERAND;
239
+ }
240
+ return null;
241
+ }
242
+ return {
243
+ UnaryTest(node) {
244
+ if (node.op !== TsVarSet)
245
+ return;
246
+ context.report({
247
+ node,
248
+ messageId: 'testV',
249
+ data: { ...gate, name: nameOf(node.x) },
250
+ });
251
+ },
252
+ CallExpr(node) {
253
+ const invoked = sourceCode.resolveInvokedCommand(node, {
254
+ transparentWrappers: TRANSPARENT_WRAPPERS,
255
+ });
256
+ if (invoked?.effective.static !== true)
257
+ return;
258
+ const command = invoked.effective.text;
259
+ if (!TEST_COMMANDS.has(command))
260
+ return;
261
+ // `chainIndices` index `node.args` directly, so slice before filtering.
262
+ let words = node.args.slice((invoked.chainIndices.at(-1) ?? 0) + 1);
263
+ if (command === '[') {
264
+ const last = words.at(-1);
265
+ if (last === undefined || sourceCode.resolveWordText(last) !== ']')
266
+ return;
267
+ words = words.slice(0, -1);
268
+ }
269
+ const texts = words.map((word) => argumentText(word));
270
+ for (const index of variableTestIndices(texts)) {
271
+ context.report({
272
+ node: words[index],
273
+ messageId: 'testV',
274
+ data: { ...gate, name: nameOf(words[index + 1]) },
275
+ });
276
+ }
277
+ },
278
+ };
279
+ },
280
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Message ids this rule reports with: `unicodeEscape` when the target leaves
3
+ * the escape undecoded, `unicodeEscapeLatin1` when it decodes it to a
4
+ * Latin-1 byte.
5
+ *
6
+ * @internal
7
+ */
8
+ export type NoUnicodeEscapeMessageId = 'unicodeEscape' | 'unicodeEscapeLatin1';
9
+ /**
10
+ * The `sh/compat/no-unicode-escape` rule module.
11
+ *
12
+ * @internal
13
+ */
14
+ export declare const noUnicodeEscape: import("../define-shell-rule.js").ShellRule<[], NoUnicodeEscapeMessageId>;