@eslint-sh/eslint-plugin 0.5.0 → 0.7.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.
- package/README.md +228 -17
- package/dist/command-harvest/builtins/bash.d.ts +36 -0
- package/dist/command-harvest/builtins/bash.js +98 -0
- package/dist/command-harvest/builtins/bats.d.ts +45 -0
- package/dist/command-harvest/builtins/bats.js +33 -0
- package/dist/command-harvest/builtins/index.d.ts +18 -0
- package/dist/command-harvest/builtins/index.js +39 -0
- package/dist/command-harvest/builtins/posix.d.ts +51 -0
- package/dist/command-harvest/builtins/posix.js +93 -0
- package/dist/command-harvest/harvest-commands.d.ts +66 -0
- package/dist/command-harvest/harvest-commands.js +97 -0
- package/dist/command-harvest/wrapper-table.d.ts +24 -0
- package/dist/command-harvest/wrapper-table.js +49 -0
- package/dist/configs/agent-authored.d.ts +41 -0
- package/dist/configs/agent-authored.js +33 -0
- package/dist/configs/bash-3-2.d.ts +28 -0
- package/dist/configs/bash-3-2.js +51 -0
- package/dist/configs/posix-portable.d.ts +40 -0
- package/dist/configs/posix-portable.js +53 -0
- package/dist/configs/recommended.d.ts +52 -0
- package/dist/configs/recommended.js +34 -0
- package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
- package/dist/declaration-conventions/comment-pragma.js +153 -0
- package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
- package/dist/declaration-conventions/declaration-convention.js +1 -0
- package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
- package/dist/declaration-conventions/read-declared-commands.js +128 -0
- package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
- package/dist/eslint-plugin-syntax.d.ts +27 -0
- package/dist/eslint-plugin.d.ts +1211 -52
- package/dist/index.d.ts +56 -8
- package/dist/index.js +53 -6
- package/dist/language/command-resolution.d.ts +124 -0
- package/dist/language/command-resolution.js +120 -0
- package/dist/language/end-of-input-marker.d.ts +29 -0
- package/dist/language/end-of-input-marker.js +88 -0
- package/dist/language/line-breaks.d.ts +18 -0
- package/dist/language/line-breaks.js +22 -0
- package/dist/language/lint-view-types.d.ts +104 -0
- package/dist/language/lint-view-types.js +1 -0
- package/dist/language/non-shell-file-guard.d.ts +35 -0
- package/dist/language/non-shell-file-guard.js +59 -0
- package/dist/language/selector-classes.d.ts +0 -1
- package/dist/language/selector-classes.js +0 -1
- package/dist/language/shell-language-options.d.ts +7 -1
- package/dist/language/shell-language-options.js +4 -4
- package/dist/language/shell-language.d.ts +10 -2
- package/dist/language/shell-language.js +59 -8
- package/dist/language/shell-source-code.d.ts +342 -5
- package/dist/language/shell-source-code.js +559 -30
- package/dist/language/source-coordinates.d.ts +13 -2
- package/dist/language/source-coordinates.js +68 -14
- package/dist/language/syntax-view.d.ts +60 -39
- package/dist/language/syntax-view.js +200 -45
- package/dist/language/word-resolution.d.ts +4 -3
- package/dist/language/word-resolution.js +4 -3
- package/dist/rules/compat/create-compat-rule.d.ts +59 -0
- package/dist/rules/compat/create-compat-rule.js +65 -0
- package/dist/rules/compat/declaration-options.d.ts +19 -0
- package/dist/rules/compat/declaration-options.js +110 -0
- package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
- package/dist/rules/compat/no-append-all-redirect.js +73 -0
- package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
- package/dist/rules/compat/no-associative-arrays.js +39 -0
- package/dist/rules/compat/no-brace-sequence-padding-step.d.ts +15 -0
- package/dist/rules/compat/no-brace-sequence-padding-step.js +287 -0
- package/dist/rules/compat/no-case-attributes.d.ts +12 -0
- package/dist/rules/compat/no-case-attributes.js +63 -0
- package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
- package/dist/rules/compat/no-case-fallthrough.js +67 -0
- package/dist/rules/compat/no-case-modification.d.ts +35 -0
- package/dist/rules/compat/no-case-modification.js +86 -0
- package/dist/rules/compat/no-coproc.d.ts +12 -0
- package/dist/rules/compat/no-coproc.js +47 -0
- package/dist/rules/compat/no-declare-g.d.ts +12 -0
- package/dist/rules/compat/no-declare-g.js +59 -0
- package/dist/rules/compat/no-double-bracket.d.ts +12 -0
- package/dist/rules/compat/no-double-bracket.js +52 -0
- package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
- package/dist/rules/compat/no-epoch-variables.js +57 -0
- package/dist/rules/compat/no-fd-variable.d.ts +12 -0
- package/dist/rules/compat/no-fd-variable.js +48 -0
- package/dist/rules/compat/no-globstar.d.ts +12 -0
- package/dist/rules/compat/no-globstar.js +39 -0
- package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
- package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
- package/dist/rules/compat/no-let.d.ts +12 -0
- package/dist/rules/compat/no-let.js +62 -0
- package/dist/rules/compat/no-mapfile.d.ts +13 -0
- package/dist/rules/compat/no-mapfile.js +122 -0
- package/dist/rules/compat/no-nameref.d.ts +12 -0
- package/dist/rules/compat/no-nameref.js +44 -0
- package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
- package/dist/rules/compat/no-negative-array-index.js +123 -0
- package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
- package/dist/rules/compat/no-nounset-empty-array.js +158 -0
- package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
- package/dist/rules/compat/no-parameter-transformation.js +123 -0
- package/dist/rules/compat/no-pipe-all.d.ts +13 -0
- package/dist/rules/compat/no-pipe-all.js +68 -0
- package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
- package/dist/rules/compat/no-printf-time-format.js +99 -0
- package/dist/rules/compat/no-source.d.ts +13 -0
- package/dist/rules/compat/no-source.js +103 -0
- package/dist/rules/compat/no-test-v.d.ts +12 -0
- package/dist/rules/compat/no-test-v.js +280 -0
- package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
- package/dist/rules/compat/no-unicode-escape.js +120 -0
- package/dist/rules/compat/no-wait-n.d.ts +12 -0
- package/dist/rules/compat/no-wait-n.js +73 -0
- package/dist/rules/compat/operator-range.d.ts +10 -0
- package/dist/rules/compat/operator-range.js +15 -0
- package/dist/rules/compat/posix-absent-command.d.ts +12 -0
- package/dist/rules/compat/posix-absent-command.js +134 -0
- package/dist/rules/compat/shopt-options.d.ts +9 -0
- package/dist/rules/compat/shopt-options.js +63 -0
- package/dist/rules/compat/version-gates.d.ts +76 -0
- package/dist/rules/compat/version-gates.js +205 -0
- package/dist/rules/create-rule.d.ts +91 -0
- package/dist/rules/create-rule.js +51 -0
- package/dist/rules/create-wrapper-rule.d.ts +166 -0
- package/dist/rules/create-wrapper-rule.js +407 -0
- package/dist/rules/define-shell-rule.d.ts +184 -0
- package/dist/rules/define-shell-rule.js +56 -0
- package/dist/rules/index.d.ts +26 -0
- package/dist/rules/index.js +90 -0
- package/dist/rules/max-script-complexity.d.ts +28 -0
- package/dist/rules/max-script-complexity.js +130 -0
- package/dist/rules/no-dynamic-invocation.d.ts +35 -0
- package/dist/rules/no-dynamic-invocation.js +117 -0
- package/dist/rules/no-eval.d.ts +13 -0
- package/dist/rules/no-eval.js +89 -0
- package/dist/rules/no-masked-exit-status.d.ts +21 -0
- package/dist/rules/no-masked-exit-status.js +667 -0
- package/dist/rules/no-secret-env-reads.d.ts +24 -0
- package/dist/rules/no-secret-env-reads.js +133 -0
- package/dist/rules/no-secret-flags.d.ts +32 -0
- package/dist/rules/no-secret-flags.js +162 -0
- package/dist/rules/no-unchecked-cd.d.ts +44 -0
- package/dist/rules/no-unchecked-cd.js +250 -0
- package/dist/rules/no-undeclared-commands.d.ts +20 -0
- package/dist/rules/no-undeclared-commands.js +115 -0
- package/dist/rules/no-unused-declarations.d.ts +23 -0
- package/dist/rules/no-unused-declarations.js +259 -0
- package/dist/rules/prefer-local.d.ts +16 -0
- package/dist/rules/prefer-local.js +249 -0
- package/dist/rules/quote-expansions.d.ts +13 -0
- package/dist/rules/quote-expansions.js +229 -0
- package/dist/rules/require-network-wrapper.d.ts +40 -0
- package/dist/rules/require-network-wrapper.js +244 -0
- package/dist/rules/require-shebang.d.ts +29 -0
- package/dist/rules/require-shebang.js +101 -0
- package/dist/rules/require-strict-mode.d.ts +52 -0
- package/dist/rules/require-strict-mode.js +177 -0
- package/dist/rules/require-tty-guard.d.ts +13 -0
- package/dist/rules/require-tty-guard.js +318 -0
- package/dist/rules/resolve-declaration-convention.d.ts +96 -0
- package/dist/rules/resolve-declaration-convention.js +109 -0
- package/dist/rules/resolve-network-wrapper.d.ts +19 -0
- package/dist/rules/resolve-network-wrapper.js +116 -0
- package/dist/rules/set-options.d.ts +109 -0
- package/dist/rules/set-options.js +201 -0
- package/dist/rules/shebang-interpreter.d.ts +18 -0
- package/dist/rules/shebang-interpreter.js +81 -0
- package/dist/rules/shebang-matches-dialect.d.ts +12 -0
- package/dist/rules/shebang-matches-dialect.js +69 -0
- package/dist/rules/transparent-wrappers.d.ts +52 -0
- package/dist/rules/transparent-wrappers.js +99 -0
- package/dist/rules/user-pattern.d.ts +63 -0
- package/dist/rules/user-pattern.js +184 -0
- package/dist/syntax/analyze.d.ts +19 -0
- package/dist/syntax/analyze.js +21 -0
- package/dist/syntax/index.d.ts +23 -0
- package/dist/syntax/index.js +25 -0
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -1
- package/docs/compat.md +354 -0
- package/docs/custom-rule-tutorial.md +780 -34
- package/docs/declaration-conventions.md +478 -0
- package/docs/examples/audit-wrapper-pack.mjs +15 -0
- package/docs/examples/dep-line-convention.mjs +22 -0
- package/docs/examples/eslint.config.mjs +3 -2
- package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
- package/docs/examples/no-backticks-rule-tester.mjs +106 -0
- package/docs/examples/no-backticks-rule.mjs +60 -0
- package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
- package/docs/examples/no-function-keyword-rule.mjs +59 -0
- package/docs/examples/no-listed-commands-rule.ts +63 -0
- package/docs/examples/no-rm-rf-rule.mjs +26 -15
- package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
- package/docs/examples/sidecar-convention.mjs +38 -0
- package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
- package/docs/examples/sorted-declarations-rule.mjs +52 -0
- package/docs/reading-nodes.md +159 -0
- package/docs/rules/README.md +79 -0
- package/docs/rules/_template.md +83 -0
- package/docs/rules/compat/no-append-all-redirect.md +87 -0
- package/docs/rules/compat/no-associative-arrays.md +95 -0
- package/docs/rules/compat/no-brace-sequence-padding-step.md +90 -0
- package/docs/rules/compat/no-case-attributes.md +82 -0
- package/docs/rules/compat/no-case-fallthrough.md +90 -0
- package/docs/rules/compat/no-case-modification.md +74 -0
- package/docs/rules/compat/no-coproc.md +76 -0
- package/docs/rules/compat/no-declare-g.md +80 -0
- package/docs/rules/compat/no-double-bracket.md +72 -0
- package/docs/rules/compat/no-epoch-variables.md +79 -0
- package/docs/rules/compat/no-fd-variable.md +78 -0
- package/docs/rules/compat/no-globstar.md +74 -0
- package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
- package/docs/rules/compat/no-let.md +78 -0
- package/docs/rules/compat/no-mapfile.md +118 -0
- package/docs/rules/compat/no-nameref.md +81 -0
- package/docs/rules/compat/no-negative-array-index.md +87 -0
- package/docs/rules/compat/no-nounset-empty-array.md +103 -0
- package/docs/rules/compat/no-parameter-transformation.md +94 -0
- package/docs/rules/compat/no-pipe-all.md +85 -0
- package/docs/rules/compat/no-printf-time-format.md +75 -0
- package/docs/rules/compat/no-source.md +98 -0
- package/docs/rules/compat/no-test-v.md +82 -0
- package/docs/rules/compat/no-unicode-escape.md +91 -0
- package/docs/rules/compat/no-wait-n.md +85 -0
- package/docs/rules/max-script-complexity.md +109 -0
- package/docs/rules/no-dynamic-invocation.md +139 -0
- package/docs/rules/no-eval.md +106 -0
- package/docs/rules/no-masked-exit-status.md +239 -0
- package/docs/rules/no-secret-env-reads.md +173 -0
- package/docs/rules/no-secret-flags.md +169 -0
- package/docs/rules/no-unchecked-cd.md +146 -0
- package/docs/rules/no-undeclared-commands.md +141 -0
- package/docs/rules/no-unused-declarations.md +216 -0
- package/docs/rules/prefer-local.md +221 -0
- package/docs/rules/quote-expansions.md +157 -0
- package/docs/rules/require-network-wrapper.md +171 -0
- package/docs/rules/require-shebang.md +157 -0
- package/docs/rules/require-strict-mode.md +253 -0
- package/docs/rules/require-tty-guard.md +131 -0
- package/docs/rules/shebang-matches-dialect.md +120 -0
- package/docs/selectors.md +2 -2
- package/docs/shellcheck.md +87 -0
- package/docs/untrusted-code.md +440 -0
- package/docs/wrapper-rules.md +206 -0
- package/package.json +26 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/language/selector-classes.d.ts.map +0 -1
- package/dist/language/selector-classes.js.map +0 -1
- package/dist/language/shell-language-options.d.ts.map +0 -1
- package/dist/language/shell-language-options.js.map +0 -1
- package/dist/language/shell-language.d.ts.map +0 -1
- package/dist/language/shell-language.js.map +0 -1
- package/dist/language/shell-source-code.d.ts.map +0 -1
- package/dist/language/shell-source-code.js.map +0 -1
- package/dist/language/source-coordinates.d.ts.map +0 -1
- package/dist/language/source-coordinates.js.map +0 -1
- package/dist/language/syntax-view.d.ts.map +0 -1
- package/dist/language/syntax-view.js.map +0 -1
- package/dist/language/word-resolution.d.ts.map +0 -1
- package/dist/language/word-resolution.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a POSIX-only compat rule finds a use of a command word POSIX sh lacks.
|
|
3
|
+
*
|
|
4
|
+
* Under `dialect: "posix"` the parser has no `[[` clause, `let` clause or
|
|
5
|
+
* similar: it reads `[[ a ]]`, `let n=1` and `source f` as ordinary commands,
|
|
6
|
+
* so the rules recognize them by the command they run. That reading is shared
|
|
7
|
+
* here so the three rules agree on what counts as a use:
|
|
8
|
+
*
|
|
9
|
+
* - **It is the effective command**, after following transparent wrappers,
|
|
10
|
+
* which is what actually runs (`command let n=1` fails on dash as `let n=1`
|
|
11
|
+
* does). `command -v let` asks a question instead of running `let`, and
|
|
12
|
+
* resolves to no such command.
|
|
13
|
+
* - **A shell without the construct can reach it.** These constructs are
|
|
14
|
+
* commands, not syntax, so a POSIX shell only fails when it runs one. A script
|
|
15
|
+
* meant for `sh` can hold a branch for one shell, and dash never enters it:
|
|
16
|
+
* `if [ -n "${BASH_VERSION-}" ]; then source ~/.bashrc; fi`. Such a use is not
|
|
17
|
+
* reported, because the finding's claim would be false.
|
|
18
|
+
*
|
|
19
|
+
* Recognizing that branch is deliberately shallow and syntactic, like the guard
|
|
20
|
+
* in `require-tty-guard`: no dataflow, and only the shapes {@link isGuarded}
|
|
21
|
+
* lists, each testing one of `BASH_VERSION`, `KSH_VERSION` or `ZSH_VERSION`
|
|
22
|
+
* directly. Anything else, including a version captured in another variable,
|
|
23
|
+
* reports.
|
|
24
|
+
*/
|
|
25
|
+
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
26
|
+
import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
|
|
27
|
+
/**
|
|
28
|
+
* The wrappers followed to find the command an invocation runs:
|
|
29
|
+
* `sh-ast/analyze`'s defaults (which include `command`) plus Bash's `builtin`.
|
|
30
|
+
*/
|
|
31
|
+
const TRANSPARENT_WRAPPERS = [
|
|
32
|
+
...DEFAULT_TRANSPARENT_WRAPPERS,
|
|
33
|
+
BUILTIN_WRAPPER,
|
|
34
|
+
];
|
|
35
|
+
/** `sh-ast`'s `BinCmdOperator` codes for `&&` and `||`, which the lint view preserves. */
|
|
36
|
+
const AND_STMT = 11;
|
|
37
|
+
const OR_STMT = 12;
|
|
38
|
+
/**
|
|
39
|
+
* The source text of a word that reads a shell's own version variable: `$X`,
|
|
40
|
+
* `${X}`, `${X-}` or `${X:-}`, quoted or not. dash sets none of these variables.
|
|
41
|
+
*/
|
|
42
|
+
const SHELL_VERSION_WORD = /^"?\$(?:\{(?:BASH|KSH|ZSH)_VERSION(?::?-)?\}|(?:BASH|KSH|ZSH)_VERSION)"?$/u;
|
|
43
|
+
/**
|
|
44
|
+
* What `stmt` tests, when it is exactly `[ -n V ]`, `test -n V` (`set`) or the
|
|
45
|
+
* `-z` forms (`unset`) for a version variable `V`, not negated. Anything else
|
|
46
|
+
* is `undefined`.
|
|
47
|
+
*/
|
|
48
|
+
function shellVersionTest(sourceCode, stmt) {
|
|
49
|
+
if (stmt?.type !== 'Stmt' || stmt.negated || stmt.cmd?.type !== 'CallExpr')
|
|
50
|
+
return undefined;
|
|
51
|
+
const { args } = stmt.cmd;
|
|
52
|
+
const name = sourceCode.getCommandName(stmt.cmd);
|
|
53
|
+
if (args.length !== (name === '[' ? 4 : name === 'test' ? 3 : 0))
|
|
54
|
+
return undefined;
|
|
55
|
+
if (name === '[' && sourceCode.resolveWordText(args[3]) !== ']')
|
|
56
|
+
return undefined;
|
|
57
|
+
if (!SHELL_VERSION_WORD.test(sourceCode.getText(args[2])))
|
|
58
|
+
return undefined;
|
|
59
|
+
const operator = sourceCode.resolveWordText(args[1]);
|
|
60
|
+
if (operator === '-n')
|
|
61
|
+
return 'set';
|
|
62
|
+
return operator === '-z' ? 'unset' : undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether `stmt` leaves the script or function unless a version variable is
|
|
66
|
+
* set: `[ -n V ] || exit` or `[ -z V ] && exit`, with `exit` or `return`.
|
|
67
|
+
*/
|
|
68
|
+
function leavesUnlessVersioned(sourceCode, stmt) {
|
|
69
|
+
if (stmt.type !== 'Stmt' || stmt.cmd?.type !== 'BinaryCmd')
|
|
70
|
+
return false;
|
|
71
|
+
const { op, x, y } = stmt.cmd;
|
|
72
|
+
const test = shellVersionTest(sourceCode, x);
|
|
73
|
+
if (!((op === OR_STMT && test === 'set') || (op === AND_STMT && test === 'unset')))
|
|
74
|
+
return false;
|
|
75
|
+
if (y?.cmd?.type !== 'CallExpr')
|
|
76
|
+
return false;
|
|
77
|
+
const name = sourceCode.getCommandName(y.cmd);
|
|
78
|
+
return name === 'exit' || name === 'return';
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether only a shell that sets a version variable can run `node`, because it
|
|
82
|
+
* sits in one of these:
|
|
83
|
+
*
|
|
84
|
+
* - the `then` body of `if [ -n V ]`, or the `else` body of `if [ -z V ]`;
|
|
85
|
+
* - the right side of `[ -n V ] && …` or `[ -z V ] || …`;
|
|
86
|
+
* - a statement list, after an earlier `[ -n V ] || exit` or `[ -z V ] && exit`
|
|
87
|
+
* in that list (the file, or a `{ … }` block such as a function body).
|
|
88
|
+
*/
|
|
89
|
+
function isGuarded(sourceCode, node) {
|
|
90
|
+
let child = node;
|
|
91
|
+
for (let parent = sourceCode.getParent(child); parent; parent = sourceCode.getParent(child)) {
|
|
92
|
+
if (parent.type === 'IfClause' && parent.cond.length === 1) {
|
|
93
|
+
const test = shellVersionTest(sourceCode, parent.cond[0]);
|
|
94
|
+
if (test === 'set' && parent.then.some((stmt) => stmt === child))
|
|
95
|
+
return true;
|
|
96
|
+
if (test === 'unset' && parent.else === child)
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
else if (parent.type === 'BinaryCmd' && parent.y === child) {
|
|
100
|
+
const test = shellVersionTest(sourceCode, parent.x);
|
|
101
|
+
if ((parent.op === AND_STMT && test === 'set') ||
|
|
102
|
+
(parent.op === OR_STMT && test === 'unset')) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (parent.type === 'File' || parent.type === 'Block') {
|
|
107
|
+
const index = parent.stmts.findIndex((stmt) => stmt === child);
|
|
108
|
+
if (parent.stmts.slice(0, index).some((stmt) => leavesUnlessVersioned(sourceCode, stmt))) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
child = parent;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The word that names `command` in `node`, when `command` is what `node`
|
|
118
|
+
* effectively runs and a shell without it can reach `node` (see the module
|
|
119
|
+
* comment), or `undefined` otherwise. Reporting this word rather than the whole
|
|
120
|
+
* command matters for `[[`: the parser ends that command at the first `&&` or
|
|
121
|
+
* `||`, so the node would cover an arbitrary part of the expression.
|
|
122
|
+
*
|
|
123
|
+
* @internal
|
|
124
|
+
*/
|
|
125
|
+
export function reachableCommandWord(sourceCode, node, command) {
|
|
126
|
+
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
127
|
+
transparentWrappers: TRANSPARENT_WRAPPERS,
|
|
128
|
+
});
|
|
129
|
+
if (invoked?.effective.static !== true || invoked.effective.text !== command)
|
|
130
|
+
return undefined;
|
|
131
|
+
if (isGuarded(sourceCode, node))
|
|
132
|
+
return undefined;
|
|
133
|
+
return node.args[invoked.chainIndices.at(-1) ?? 0];
|
|
134
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ShellNode } from '../../language/syntax-view.js';
|
|
2
|
+
import type { ShellRuleContext, ShellRuleVisitor } from '../define-shell-rule.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the listeners that call `report(word)` for each operand word of a
|
|
5
|
+
* `shopt -s` command whose static text is `option`.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare function shoptEnableVisitor<MessageIds extends string>(context: ShellRuleContext<[], MessageIds>, option: string, report: (word: ShellNode) => void): ShellRuleVisitor;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared detection of `shopt -s NAME`, for the compat rules that gate one
|
|
3
|
+
* `shopt` option (`lastpipe`, `globstar`).
|
|
4
|
+
*
|
|
5
|
+
* What is read: a `shopt` command (after transparent wrappers such as
|
|
6
|
+
* `builtin`) whose option words, up to the first operand, include `-s`
|
|
7
|
+
* (possibly inside a cluster such as `-sq`). Its operands are the option
|
|
8
|
+
* names it turns on. `shopt -u` turns options off (so does a `-u` beside the
|
|
9
|
+
* `-s`), `shopt -o` names `set -o` options, and an operand that is not a
|
|
10
|
+
* static word cannot be read, so none of those count.
|
|
11
|
+
*/
|
|
12
|
+
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
13
|
+
import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
|
|
14
|
+
/**
|
|
15
|
+
* The wrappers followed to find the `shopt` command: `sh-ast/analyze`'s
|
|
16
|
+
* defaults (which include `command`) plus Bash's `builtin`.
|
|
17
|
+
*/
|
|
18
|
+
const TRANSPARENT_WRAPPERS = [
|
|
19
|
+
...DEFAULT_TRANSPARENT_WRAPPERS,
|
|
20
|
+
BUILTIN_WRAPPER,
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Builds the listeners that call `report(word)` for each operand word of a
|
|
24
|
+
* `shopt -s` command whose static text is `option`.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export function shoptEnableVisitor(context, option, report) {
|
|
29
|
+
const { sourceCode } = context;
|
|
30
|
+
return {
|
|
31
|
+
CallExpr(node) {
|
|
32
|
+
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
33
|
+
transparentWrappers: TRANSPARENT_WRAPPERS,
|
|
34
|
+
});
|
|
35
|
+
if (invoked?.effective.static !== true || invoked.effective.text !== 'shopt')
|
|
36
|
+
return;
|
|
37
|
+
// `chainIndices` index `node.args` directly, so slice before filtering.
|
|
38
|
+
const words = node.args.slice((invoked.chainIndices.at(-1) ?? 0) + 1);
|
|
39
|
+
let enables = false;
|
|
40
|
+
let disables = false;
|
|
41
|
+
let namesShellOptions = false;
|
|
42
|
+
let index = 0;
|
|
43
|
+
for (; index < words.length; index++) {
|
|
44
|
+
const text = sourceCode.resolveWordText(words[index]);
|
|
45
|
+
if (text === null || !/^-./u.test(text))
|
|
46
|
+
break;
|
|
47
|
+
if (text === '--') {
|
|
48
|
+
index++;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
enables ||= text.includes('s');
|
|
52
|
+
disables ||= text.includes('u');
|
|
53
|
+
namesShellOptions ||= text.includes('o');
|
|
54
|
+
}
|
|
55
|
+
if (!enables || disables || namesShellOptions)
|
|
56
|
+
return;
|
|
57
|
+
for (const word of words.slice(index)) {
|
|
58
|
+
if (sourceCode.resolveWordText(word) === option)
|
|
59
|
+
report(word);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The version-gate table every `compat/*` rule reads, and the one function
|
|
3
|
+
* that decides whether a gated feature is available to a file.
|
|
4
|
+
*
|
|
5
|
+
* A gate is data: a feature name mapped to the oldest Bash release that has
|
|
6
|
+
* it and whether POSIX sh has it. A new gate is a new row here, not new
|
|
7
|
+
* decision logic in a rule. The semantics of `languageOptions.shellVersion`
|
|
8
|
+
* and `languageOptions.dialect` for gating are defined once, by
|
|
9
|
+
* {@link isFeatureAvailable}, and documented for users in `docs/compat.md`.
|
|
10
|
+
*
|
|
11
|
+
* Every row is checked against real shells by `scripts/compat-matrix/run.sh`,
|
|
12
|
+
* which builds each Bash release and dash from source and runs one probe per
|
|
13
|
+
* feature on each; the observed results are the Evidence table in
|
|
14
|
+
* `docs/compat.md`. A changed row needs a probe there that shows the new
|
|
15
|
+
* boundary.
|
|
16
|
+
*
|
|
17
|
+
* @see https://tiswww.case.edu/php/chet/bash/NEWS — Bash release notes, the
|
|
18
|
+
* source of every minimum version below
|
|
19
|
+
* @see https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html
|
|
20
|
+
* — POSIX Shell Command Language: section 2.4 (reserved words) and section
|
|
21
|
+
* 2.9.1.4 (command search) leave the results of the `doubleBracket`,
|
|
22
|
+
* `letCommand` and `sourceCommand` rows unspecified
|
|
23
|
+
*/
|
|
24
|
+
import type { ShellLanguageOptions } from '../../language/shell-language-options.js';
|
|
25
|
+
/**
|
|
26
|
+
* The name of a gated feature: a row of {@link FEATURE_GATES}.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export type CompatFeature = 'appendAllRedirect' | 'associativeArrays' | 'braceSequencePaddingStep' | 'caseAttributes' | 'caseFallthrough' | 'caseModification' | 'coproc' | 'declareGlobal' | 'doubleBracket' | 'epochVariables' | 'fdVariable' | 'globstar' | 'lastpipe' | 'letCommand' | 'mapfile' | 'nameref' | 'negativeArrayIndex' | 'nounsetEmptyArray' | 'parameterTransformation' | 'parameterTransformationCase' | 'parameterTransformationKeys' | 'parameterTransformationQuotedKeys' | 'pipeAll' | 'printfTimeFormat' | 'sourceCommand' | 'testVariableSet' | 'unicodeEscape' | 'unicodeEscapeLatin1' | 'waitNext';
|
|
31
|
+
/**
|
|
32
|
+
* The two `languageOptions` fields gating reads. Both the raw and the
|
|
33
|
+
* normalized options satisfy it; an unset `dialect` means `"bash"`, as it
|
|
34
|
+
* does for the parser.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export type GateLanguageOptions = Pick<ShellLanguageOptions, 'dialect' | 'shellVersion'>;
|
|
39
|
+
/**
|
|
40
|
+
* Whether `feature` is available to a file linted with `languageOptions`.
|
|
41
|
+
*
|
|
42
|
+
* - `dialect: "posix"`: available only when the gate's `posix` is `true`,
|
|
43
|
+
* at any `shellVersion` (a Bash-only feature is never available).
|
|
44
|
+
* - `dialect: "bash"` (the default) or `"bats"`: an unset `shellVersion`
|
|
45
|
+
* means the newest Bash, so every Bash feature is available. A set
|
|
46
|
+
* `shellVersion` makes the feature available when it is the gate's `bash`
|
|
47
|
+
* minimum or newer, comparing major then minor numerically (`"4"` is
|
|
48
|
+
* `4.0`; `"3.10"` is newer than `"3.2"`). A `null` minimum is available at
|
|
49
|
+
* every version.
|
|
50
|
+
* - `dialect: "mksh"` or `"zsh"`: the table carries no data for these
|
|
51
|
+
* shells, and their `shellVersion` numbers their own releases, not Bash's,
|
|
52
|
+
* so no gate applies and every feature is available.
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
export declare function isFeatureAvailable(feature: CompatFeature, languageOptions: GateLanguageOptions): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Human-readable words for a gate's report: what the feature needs
|
|
59
|
+
* (`required`, such as `bash 4.0 or newer`) and what the file targets
|
|
60
|
+
* (`target`, such as `bash 3.2` or `POSIX sh`). Compat rule messages
|
|
61
|
+
* interpolate both as `{{required}}` and `{{target}}`.
|
|
62
|
+
*
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
export interface GateDescription {
|
|
66
|
+
readonly required: string;
|
|
67
|
+
readonly target: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Describes why `feature` is unavailable under `languageOptions`, for a
|
|
71
|
+
* report's message data. Call it only when {@link isFeatureAvailable} is
|
|
72
|
+
* `false`.
|
|
73
|
+
*
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
export declare function describeGate(feature: CompatFeature, languageOptions: GateLanguageOptions): GateDescription;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every gated feature, keyed by the name rules pass to
|
|
3
|
+
* {@link isFeatureAvailable}.
|
|
4
|
+
*/
|
|
5
|
+
const FEATURE_GATES = Object.freeze({
|
|
6
|
+
/**
|
|
7
|
+
* The `&>>` redirection, appending both stdout and stderr: Bash 4.0 NEWS,
|
|
8
|
+
* "the new `&>>` redirection operator". Bash 3.2 rejects it as a syntax
|
|
9
|
+
* error, so the whole script fails to parse.
|
|
10
|
+
*/
|
|
11
|
+
appendAllRedirect: Object.freeze({ bash: '4.0', posix: false }),
|
|
12
|
+
/** `declare -A` / `typeset -A`: Bash 4.0 NEWS, "associative arrays". */
|
|
13
|
+
associativeArrays: Object.freeze({ bash: '4.0', posix: false }),
|
|
14
|
+
/**
|
|
15
|
+
* Zero-padded (`{01..10}`) and stepped (`{1..10..2}`, `{a..e..2}`) brace
|
|
16
|
+
* sequences: Bash 4.0 NEWS (brace expansion gained zero-padding and an
|
|
17
|
+
* increment). Bash 3.2 expands the padded form without the padding and
|
|
18
|
+
* leaves the stepped form as a literal word; POSIX sh has no brace
|
|
19
|
+
* expansion.
|
|
20
|
+
*/
|
|
21
|
+
braceSequencePaddingStep: Object.freeze({ bash: '4.0', posix: false }),
|
|
22
|
+
/**
|
|
23
|
+
* The case-modifying expansions `${v^}`, `${v^^}`, `${v,}`, `${v,,}`: Bash
|
|
24
|
+
* 4.0 NEWS, "new case-modifying word expansions".
|
|
25
|
+
*/
|
|
26
|
+
caseModification: Object.freeze({ bash: '4.0', posix: false }),
|
|
27
|
+
/**
|
|
28
|
+
* The case-converting attributes `declare -l` / `-u` (and through
|
|
29
|
+
* `typeset` and `local`): Bash 4.0 NEWS, "new `-l` and `-u` options to
|
|
30
|
+
* declare/typeset/local".
|
|
31
|
+
*/
|
|
32
|
+
caseAttributes: Object.freeze({ bash: '4.0', posix: false }),
|
|
33
|
+
/**
|
|
34
|
+
* The `case` terminators `;&` (fall through) and `;;&` (test the next
|
|
35
|
+
* pattern): Bash 4.0 NEWS. POSIX.1-2024 adds `;&`, but dash 0.5.12 still
|
|
36
|
+
* rejects it, so a POSIX target does not have it.
|
|
37
|
+
*/
|
|
38
|
+
caseFallthrough: Object.freeze({ bash: '4.0', posix: false }),
|
|
39
|
+
/** The `coproc` reserved word: Bash 4.0 NEWS, "a new `coproc` reserved word". */
|
|
40
|
+
coproc: Object.freeze({ bash: '4.0', posix: false }),
|
|
41
|
+
/**
|
|
42
|
+
* `declare -g` / `typeset -g`: Bash 4.2 NEWS, "declare/typeset has a new
|
|
43
|
+
* `-g` option".
|
|
44
|
+
*/
|
|
45
|
+
declareGlobal: Object.freeze({ bash: '4.2', posix: false }),
|
|
46
|
+
/**
|
|
47
|
+
* The `[[ … ]]` conditional. POSIX section 2.4 lists `[[` and `]]` among the
|
|
48
|
+
* words whose results are unspecified; every Bash has it, and dash runs it
|
|
49
|
+
* as a command that does not exist.
|
|
50
|
+
*/
|
|
51
|
+
doubleBracket: Object.freeze({ bash: null, posix: false }),
|
|
52
|
+
/** `$EPOCHSECONDS` and `$EPOCHREALTIME`: Bash 5.0 NEWS. */
|
|
53
|
+
epochVariables: Object.freeze({ bash: '5.0', posix: false }),
|
|
54
|
+
/**
|
|
55
|
+
* A `{varname}` redirection that allocates a descriptor into a variable
|
|
56
|
+
* (`exec {fd}>file`): Bash 4.1 NEWS. Bash 4.0 runs `{fd}` as a command.
|
|
57
|
+
*/
|
|
58
|
+
fdVariable: Object.freeze({ bash: '4.1', posix: false }),
|
|
59
|
+
/** `shopt -s globstar` (recursive `**`): Bash 4.0 NEWS. */
|
|
60
|
+
globstar: Object.freeze({ bash: '4.0', posix: false }),
|
|
61
|
+
/** `shopt -s lastpipe`: Bash 4.2 NEWS, "a new `lastpipe` shell option". */
|
|
62
|
+
lastpipe: Object.freeze({ bash: '4.2', posix: false }),
|
|
63
|
+
/**
|
|
64
|
+
* The `let` builtin. POSIX section 2.9.1.4 lists `let` among the command
|
|
65
|
+
* names whose results are unspecified; every Bash has it, and dash has no
|
|
66
|
+
* such command.
|
|
67
|
+
*/
|
|
68
|
+
letCommand: Object.freeze({ bash: null, posix: false }),
|
|
69
|
+
/** The `mapfile` builtin and its `readarray` synonym: Bash 4.0 NEWS. */
|
|
70
|
+
mapfile: Object.freeze({ bash: '4.0', posix: false }),
|
|
71
|
+
/** Namerefs, `declare -n` / `typeset -n` / `local -n`: Bash 4.3 NEWS. */
|
|
72
|
+
nameref: Object.freeze({ bash: '4.3', posix: false }),
|
|
73
|
+
/**
|
|
74
|
+
* Negative indexed-array subscripts (`${a[-1]}`, `a[-1]=x`): Bash 4.3 NEWS,
|
|
75
|
+
* "assigning, referencing, and unsetting elements of indexed arrays using
|
|
76
|
+
* negative subscripts". Bash 4.2 already reads `${a[-1]}`, but rejects the
|
|
77
|
+
* element length `${#a[-1]}`, assignment and unset, so 4.3 is the first
|
|
78
|
+
* release where every use works.
|
|
79
|
+
*/
|
|
80
|
+
negativeArrayIndex: Object.freeze({ bash: '4.3', posix: false }),
|
|
81
|
+
/**
|
|
82
|
+
* Expanding an empty array (`"${a[@]}"`, `${a[*]}`) while `set -u` is on:
|
|
83
|
+
* the syntax is in every Bash, but through 4.3 it aborts with "unbound
|
|
84
|
+
* variable"; Bash 4.4 is the first release where it expands to nothing.
|
|
85
|
+
*/
|
|
86
|
+
nounsetEmptyArray: Object.freeze({ bash: '4.4', posix: false }),
|
|
87
|
+
/**
|
|
88
|
+
* The parameter transformations `${v@Q}`, `${v@E}`, `${v@P}`, `${v@A}` and
|
|
89
|
+
* `${v@a}`: Bash 4.4 NEWS.
|
|
90
|
+
*/
|
|
91
|
+
parameterTransformation: Object.freeze({ bash: '4.4', posix: false }),
|
|
92
|
+
/** The case transformations `${v@U}`, `${v@u}` and `${v@L}`: Bash 5.1 NEWS. */
|
|
93
|
+
parameterTransformationCase: Object.freeze({ bash: '5.1', posix: false }),
|
|
94
|
+
/** `${a[@]@k}`, key-value pairs as separate words: Bash 5.2 NEWS. */
|
|
95
|
+
parameterTransformationKeys: Object.freeze({ bash: '5.2', posix: false }),
|
|
96
|
+
/** `${a[@]@K}`, quoted key-value pairs: Bash 5.1 NEWS. */
|
|
97
|
+
parameterTransformationQuotedKeys: Object.freeze({ bash: '5.1', posix: false }),
|
|
98
|
+
/**
|
|
99
|
+
* The `|&` pipe, shorthand for `2>&1 |`: Bash 4.0 NEWS. Bash 3.2 rejects
|
|
100
|
+
* it as a syntax error.
|
|
101
|
+
*/
|
|
102
|
+
pipeAll: Object.freeze({ bash: '4.0', posix: false }),
|
|
103
|
+
/** `printf`'s `%(fmt)T` date format: Bash 4.2 NEWS. */
|
|
104
|
+
printfTimeFormat: Object.freeze({ bash: '4.2', posix: false }),
|
|
105
|
+
/**
|
|
106
|
+
* The `source` builtin, Bash's name for `.`. POSIX section 2.9.1.4 lists
|
|
107
|
+
* `source` among the command names whose results are unspecified; every Bash
|
|
108
|
+
* has it, and dash has no such command.
|
|
109
|
+
*/
|
|
110
|
+
sourceCommand: Object.freeze({ bash: null, posix: false }),
|
|
111
|
+
/**
|
|
112
|
+
* The `-v name` test (`[[ -v x ]]`, `test -v x`, `[ -v x ]`): Bash 4.2
|
|
113
|
+
* NEWS. In older Bash, `[[ -v x ]]` is a syntax error.
|
|
114
|
+
*/
|
|
115
|
+
testVariableSet: Object.freeze({ bash: '4.2', posix: false }),
|
|
116
|
+
/**
|
|
117
|
+
* The `\u`/`\U` escapes in `$'…'` strings: Bash 4.2 NEWS. Earlier releases
|
|
118
|
+
* leave the escape as literal text. See `unicodeEscapeLatin1` for the one
|
|
119
|
+
* range the 4.2 release decodes wrongly.
|
|
120
|
+
*/
|
|
121
|
+
unicodeEscape: Object.freeze({ bash: '4.2', posix: false }),
|
|
122
|
+
/**
|
|
123
|
+
* A `\u`/`\U` escape for a code point from U+0080 to U+00FF: the Bash 4.2
|
|
124
|
+
* release tarball writes these as a single Latin-1 byte even in a UTF-8
|
|
125
|
+
* locale (observed: `e9` for U+00E9, where UTF-8 is `c3 a9`), while every
|
|
126
|
+
* other code point is UTF-8. Bash 4.3 is the first release that writes all
|
|
127
|
+
* of them as UTF-8.
|
|
128
|
+
*/
|
|
129
|
+
unicodeEscapeLatin1: Object.freeze({ bash: '4.3', posix: false }),
|
|
130
|
+
/** `wait -n`, waiting for the next job to finish: Bash 4.3 NEWS. */
|
|
131
|
+
waitNext: Object.freeze({ bash: '4.3', posix: false }),
|
|
132
|
+
});
|
|
133
|
+
/**
|
|
134
|
+
* Splits a `MAJOR[.MINOR]` string into numbers, so `"4"` is `[4, 0]` and
|
|
135
|
+
* `"3.10"` is `[3, 10]`. Returns `undefined` for any other shape; the
|
|
136
|
+
* language's option validation already rejects those, so the gate never
|
|
137
|
+
* sees one from a real config.
|
|
138
|
+
*/
|
|
139
|
+
function parseVersion(version) {
|
|
140
|
+
if (!/^\d+(?:\.\d+)?$/u.test(version))
|
|
141
|
+
return undefined;
|
|
142
|
+
const [major, minor = '0'] = version.split('.');
|
|
143
|
+
return [Number(major), Number(minor)];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Compares two `MAJOR[.MINOR]` versions numerically, major first, then minor:
|
|
147
|
+
* negative when `a` is older, `0` when equal, positive when `a` is newer. So
|
|
148
|
+
* `"3.10"` is newer than `"3.2"`, and `"4"` equals `"4.0"`.
|
|
149
|
+
*/
|
|
150
|
+
function compareVersions(a, b) {
|
|
151
|
+
return a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1];
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The dialects whose `shellVersion` is a Bash version. `bats` test files run
|
|
155
|
+
* under Bash, so they are gated exactly like `bash` files.
|
|
156
|
+
*/
|
|
157
|
+
const BASH_DIALECTS = new Set(['bash', 'bats']);
|
|
158
|
+
/**
|
|
159
|
+
* Whether `feature` is available to a file linted with `languageOptions`.
|
|
160
|
+
*
|
|
161
|
+
* - `dialect: "posix"`: available only when the gate's `posix` is `true`,
|
|
162
|
+
* at any `shellVersion` (a Bash-only feature is never available).
|
|
163
|
+
* - `dialect: "bash"` (the default) or `"bats"`: an unset `shellVersion`
|
|
164
|
+
* means the newest Bash, so every Bash feature is available. A set
|
|
165
|
+
* `shellVersion` makes the feature available when it is the gate's `bash`
|
|
166
|
+
* minimum or newer, comparing major then minor numerically (`"4"` is
|
|
167
|
+
* `4.0`; `"3.10"` is newer than `"3.2"`). A `null` minimum is available at
|
|
168
|
+
* every version.
|
|
169
|
+
* - `dialect: "mksh"` or `"zsh"`: the table carries no data for these
|
|
170
|
+
* shells, and their `shellVersion` numbers their own releases, not Bash's,
|
|
171
|
+
* so no gate applies and every feature is available.
|
|
172
|
+
*
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
export function isFeatureAvailable(feature, languageOptions) {
|
|
176
|
+
const gate = FEATURE_GATES[feature];
|
|
177
|
+
const dialect = languageOptions.dialect ?? 'bash';
|
|
178
|
+
if (dialect === 'posix')
|
|
179
|
+
return gate.posix;
|
|
180
|
+
if (!BASH_DIALECTS.has(dialect))
|
|
181
|
+
return true;
|
|
182
|
+
const { shellVersion } = languageOptions;
|
|
183
|
+
if (shellVersion === undefined || gate.bash === null)
|
|
184
|
+
return true;
|
|
185
|
+
const target = parseVersion(shellVersion);
|
|
186
|
+
const minimum = parseVersion(gate.bash);
|
|
187
|
+
if (target === undefined || minimum === undefined)
|
|
188
|
+
return true;
|
|
189
|
+
return compareVersions(target, minimum) >= 0;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Describes why `feature` is unavailable under `languageOptions`, for a
|
|
193
|
+
* report's message data. Call it only when {@link isFeatureAvailable} is
|
|
194
|
+
* `false`.
|
|
195
|
+
*
|
|
196
|
+
* @internal
|
|
197
|
+
*/
|
|
198
|
+
export function describeGate(feature, languageOptions) {
|
|
199
|
+
const gate = FEATURE_GATES[feature];
|
|
200
|
+
const required = gate.bash === null ? 'bash' : `bash ${gate.bash} or newer`;
|
|
201
|
+
const target = (languageOptions.dialect ?? 'bash') === 'posix'
|
|
202
|
+
? 'POSIX sh'
|
|
203
|
+
: `bash ${languageOptions.shellVersion ?? ''}`.trim();
|
|
204
|
+
return { required, target };
|
|
205
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The factory for this plugin's built-in rules: the public
|
|
3
|
+
* {@link defineShellRule} plus the one piece of metadata that only built-in
|
|
4
|
+
* rules share.
|
|
5
|
+
*
|
|
6
|
+
* `meta.docs.url` is derived from the rule name and always points at the
|
|
7
|
+
* rule's committed docs page, so the link a user follows from a lint report
|
|
8
|
+
* can never be hand-mistyped or left stale after a rename. Everything else —
|
|
9
|
+
* `meta.languages: ["sh/sh"]` and the shell-typed context, nodes and visitor —
|
|
10
|
+
* comes from `defineShellRule`, so built-in and third-party rules share one
|
|
11
|
+
* implementation of that guarantee.
|
|
12
|
+
*
|
|
13
|
+
* This module is internal (not re-exported from the package entry point): the
|
|
14
|
+
* docs URL scheme is this repository's. Rule packs use `defineShellRule` and
|
|
15
|
+
* write their own `meta.docs.url`.
|
|
16
|
+
*/
|
|
17
|
+
import type { RulesMetaDocs } from '@eslint/core';
|
|
18
|
+
import type { ShellRule, ShellRuleContext, ShellRuleMeta, ShellRuleVisitor } from './define-shell-rule.js';
|
|
19
|
+
/**
|
|
20
|
+
* The canonical documentation URL for the built-in rule `name`. This is the
|
|
21
|
+
* single place the URL scheme is spelled; the factory uses it to fill
|
|
22
|
+
* `meta.docs.url`, and the docs-page tests use it to check each registered
|
|
23
|
+
* rule points at its own committed page.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export declare function ruleDocsUrl(name: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* The documentation URL for `page`, a path relative to this package's `docs/`
|
|
30
|
+
* directory (`wrapper-rules.md` is the committed file
|
|
31
|
+
* `packages/eslint-plugin/docs/wrapper-rules.md`). Used for rules that are not
|
|
32
|
+
* in the registry, which have no `docs/rules/<name>.md` page of their own.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export declare function packageDocsUrl(page: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* The `meta.docs` an author writes: everything ESLint accepts except `url`,
|
|
39
|
+
* which the factory derives from the rule name. Typed `never` so supplying a
|
|
40
|
+
* URL by hand is a compile error rather than a silently-overwritten value.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
export type BuiltInRuleDocs = Omit<RulesMetaDocs, 'url'> & {
|
|
45
|
+
readonly url?: never;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The `meta` a built-in rule writes: {@link ShellRuleMeta} (so the language
|
|
49
|
+
* fields are already forbidden), with `docs` narrowed to
|
|
50
|
+
* {@link BuiltInRuleDocs} because the factory derives `docs.url`.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export type BuiltInRuleMeta<RuleOptions extends unknown[], MessageIds extends string> = Omit<ShellRuleMeta<RuleOptions, MessageIds>, 'docs'> & {
|
|
55
|
+
readonly docs?: BuiltInRuleDocs;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* What an author hands to {@link createRule}: the rule's registry name (used
|
|
59
|
+
* to derive its docs URL), its author-owned metadata, and its `create()`.
|
|
60
|
+
*
|
|
61
|
+
* @internal
|
|
62
|
+
*/
|
|
63
|
+
export interface BuiltInRuleSpec<RuleOptions extends unknown[], MessageIds extends string> {
|
|
64
|
+
/**
|
|
65
|
+
* The rule's name without the plugin namespace (`require-shebang`, not
|
|
66
|
+
* `sh/require-shebang`). For a built-in rule it must equal the rule's key in
|
|
67
|
+
* the registry and the basename of its docs page.
|
|
68
|
+
*/
|
|
69
|
+
readonly name: string;
|
|
70
|
+
/**
|
|
71
|
+
* The docs URL of a rule that is not in the registry. A rule made by one of
|
|
72
|
+
* this package's public factories (for example `createWrapperRule`) has no
|
|
73
|
+
* `docs/rules/<name>.md` page: it links its factory's page
|
|
74
|
+
* ({@link packageDocsUrl}), or the page its rule pack supplies. Omit it for
|
|
75
|
+
* a built-in rule, whose page is always `docs/rules/<name>.md`.
|
|
76
|
+
*/
|
|
77
|
+
readonly docsUrl?: string;
|
|
78
|
+
readonly meta: BuiltInRuleMeta<RuleOptions, MessageIds>;
|
|
79
|
+
/** Builds the rule's listeners for one file; receives the shell-typed context. */
|
|
80
|
+
readonly create: (context: ShellRuleContext<RuleOptions, MessageIds>) => ShellRuleVisitor;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Builds a built-in shell rule from its spec with {@link defineShellRule},
|
|
84
|
+
* after setting `meta.docs.url` to the rule's docs page ({@link ruleDocsUrl},
|
|
85
|
+
* or the spec's `docsUrl` for a rule outside the registry). The URL is
|
|
86
|
+
* written after the author's `docs`, so even an untyped caller cannot
|
|
87
|
+
* override it; `defineShellRule` does the same for `meta.languages`.
|
|
88
|
+
*
|
|
89
|
+
* @internal
|
|
90
|
+
*/
|
|
91
|
+
export declare function createRule<RuleOptions extends unknown[] = [], MessageIds extends string = string>(spec: BuiltInRuleSpec<RuleOptions, MessageIds>): ShellRule<RuleOptions, MessageIds>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineShellRule } from './define-shell-rule.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where this package's shipped docs live in the source repository: the
|
|
4
|
+
* committed `packages/eslint-plugin/docs/` directory.
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_DOCS_BASE_URL = 'https://github.com/mike-north/eslint-sh/blob/main/packages/eslint-plugin/docs/';
|
|
7
|
+
/**
|
|
8
|
+
* Where built-in rule docs pages live in the source repository. A rule named
|
|
9
|
+
* `foo` is documented at `<this>/foo.md`, i.e. the committed file
|
|
10
|
+
* `packages/eslint-plugin/docs/rules/foo.md`.
|
|
11
|
+
*/
|
|
12
|
+
const RULE_DOCS_BASE_URL = `${PACKAGE_DOCS_BASE_URL}rules/`;
|
|
13
|
+
/**
|
|
14
|
+
* The canonical documentation URL for the built-in rule `name`. This is the
|
|
15
|
+
* single place the URL scheme is spelled; the factory uses it to fill
|
|
16
|
+
* `meta.docs.url`, and the docs-page tests use it to check each registered
|
|
17
|
+
* rule points at its own committed page.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export function ruleDocsUrl(name) {
|
|
22
|
+
return `${RULE_DOCS_BASE_URL}${name}.md`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The documentation URL for `page`, a path relative to this package's `docs/`
|
|
26
|
+
* directory (`wrapper-rules.md` is the committed file
|
|
27
|
+
* `packages/eslint-plugin/docs/wrapper-rules.md`). Used for rules that are not
|
|
28
|
+
* in the registry, which have no `docs/rules/<name>.md` page of their own.
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export function packageDocsUrl(page) {
|
|
33
|
+
return `${PACKAGE_DOCS_BASE_URL}${page}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Builds a built-in shell rule from its spec with {@link defineShellRule},
|
|
37
|
+
* after setting `meta.docs.url` to the rule's docs page ({@link ruleDocsUrl},
|
|
38
|
+
* or the spec's `docsUrl` for a rule outside the registry). The URL is
|
|
39
|
+
* written after the author's `docs`, so even an untyped caller cannot
|
|
40
|
+
* override it; `defineShellRule` does the same for `meta.languages`.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
export function createRule(spec) {
|
|
45
|
+
const { name, docsUrl, meta, create } = spec;
|
|
46
|
+
const url = docsUrl ?? ruleDocsUrl(name);
|
|
47
|
+
return defineShellRule({
|
|
48
|
+
meta: { ...meta, docs: { ...meta.docs, url } },
|
|
49
|
+
create,
|
|
50
|
+
});
|
|
51
|
+
}
|