@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,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-brace-sequence-padding-step`: reports a brace sequence that
|
|
3
|
+
* is zero-padded (`{01..10}`) or stepped (`{1..10..2}`, `{a..e..2}`) in
|
|
4
|
+
* files whose target shell lacks those forms, that is, Bash older than 4.0
|
|
5
|
+
* (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
|
|
6
|
+
*
|
|
7
|
+
* Neither form is an error on Bash 3.2, which is what makes them easy to
|
|
8
|
+
* miss. Bash 3.2 expands `{01..03}` to `1 2 3`, dropping the padding, and
|
|
9
|
+
* leaves `{1..9..4}` as that literal word, so the script carries on with
|
|
10
|
+
* different words. POSIX does not specify brace expansion, so under a POSIX
|
|
11
|
+
* target what a shell does with the word depends on the shell (observed: dash
|
|
12
|
+
* leaves it literal, macOS's `/bin/sh` drops the padding as Bash 3.2 does,
|
|
13
|
+
* and `bash --posix` 5.3 pads); a plain `{1..3}` is as shell-dependent there,
|
|
14
|
+
* and this rule does not report it: only the Bash 4.0 additions are in its
|
|
15
|
+
* scope. The
|
|
16
|
+
* message suggests the portable alternative: count in a `while` loop and
|
|
17
|
+
* format with `printf`.
|
|
18
|
+
*
|
|
19
|
+
* What is read: the parser keeps a brace sequence as literal text inside a
|
|
20
|
+
* word, so the rule scans the literal parts of words that the shell
|
|
21
|
+
* brace-expands. A sequence is a `{` and `}` with nothing but the sequence
|
|
22
|
+
* between them (so `{01..03,x}`, a comma list, is not one), where:
|
|
23
|
+
*
|
|
24
|
+
* - the endpoints are both integers (an optional sign and digits) or both
|
|
25
|
+
* single ASCII letters, with an optional third `..` field that is an
|
|
26
|
+
* integer step: that makes it stepped, for either kind of endpoint;
|
|
27
|
+
* - integer endpoints are padded when either has more than one digit and
|
|
28
|
+
* starts with `0` after an optional `-` (`01`, `00`, `-01`, `1..03`; `0`,
|
|
29
|
+
* `-0` and `+01` are not).
|
|
30
|
+
*
|
|
31
|
+
* Anything that is not a sequence in Bash 4.0 and newer is left alone:
|
|
32
|
+
* `{a..c}` and `{1..3}` (no padding, no step, the same on every Bash),
|
|
33
|
+
* mixed endpoints (`{a..5..2}`), a variable endpoint (`{01..$n}`, which the
|
|
34
|
+
* shell never expands), an unterminated or malformed one (`{01..}`), and a
|
|
35
|
+
* `{` or `}` that is quoted or backslash-escaped, since quoted text is a
|
|
36
|
+
* separate part of the word and an escape is skipped when scanning.
|
|
37
|
+
*
|
|
38
|
+
* Numbers are bounded as Bash 5.3 bounds them, because past the bounds the
|
|
39
|
+
* word stays literal on every Bash and nothing differs: endpoints must fit a
|
|
40
|
+
* signed 64-bit integer, so must the distance between them, and so must the
|
|
41
|
+
* step's magnitude (a step of -9223372036854775808 or a value past 64 bits
|
|
42
|
+
* leaves the word literal;
|
|
43
|
+
* leading zeros do not count towards the value, so `{1..00000000000000000003}`
|
|
44
|
+
* is a sequence). Bash 3.2 reads the same words as literal or wraps them
|
|
45
|
+
* (its integers are narrower), and the older 4.x releases' bound was not
|
|
46
|
+
* measured, so a sequence inside the 64-bit range is reported and one outside
|
|
47
|
+
* it is not.
|
|
48
|
+
*
|
|
49
|
+
* Only words that are brace-expanded are read: command words and arguments,
|
|
50
|
+
* `for`/`select` lists, the elements of an array literal without an index,
|
|
51
|
+
* and the operands of `declare`, `local`, `export` and `readonly`. Not read,
|
|
52
|
+
* because the shell does no brace expansion there: a plain assignment
|
|
53
|
+
* (`a={01..03}`), a `case` subject or pattern, the operands of `[[ ]]`,
|
|
54
|
+
* here-strings and here-documents, and parameter-expansion words.
|
|
55
|
+
*
|
|
56
|
+
* A descriptor duplication target is not reported where it behaves the same
|
|
57
|
+
* on every Bash; see `isSameOnEveryBash`.
|
|
58
|
+
*
|
|
59
|
+
* Text inside a quoted string that the shell later runs as code (`eval
|
|
60
|
+
* '...'`, `trap '...'`) is not seen: the parser holds it as a string.
|
|
61
|
+
*
|
|
62
|
+
* A redirection target (`: > out{01..01}`) is read only when the sequence
|
|
63
|
+
* can be shown to yield exactly one word, since several words make it an
|
|
64
|
+
* "ambiguous redirect" error on Bash 4 and newer (on 3.2 too for a padded
|
|
65
|
+
* one), and the script is then wrong on every Bash that has the feature,
|
|
66
|
+
* not a portability gap. The word must hold that one sequence and no other
|
|
67
|
+
* brace group, whose product with it could not be counted here. A single-word
|
|
68
|
+
* result differs by target: `out{01..01}` names `out1` on Bash 3.2 and
|
|
69
|
+
* `out01` on 5.3, and `out{1..3..5}` names a file called `out{1..3..5}` on
|
|
70
|
+
* 3.2 and `out1` on 5.3. A sequence is always static text here (a variable
|
|
71
|
+
* endpoint is no sequence), so its length is always known. The report is on
|
|
72
|
+
* the whole word, once per word.
|
|
73
|
+
*/
|
|
74
|
+
import { createCompatRule } from './create-compat-rule.js';
|
|
75
|
+
import { DashHdoc, DplIn, DplOut, Hdoc, WordHdoc } from 'sh-ast/analyze';
|
|
76
|
+
/** The sequence between braces: two endpoints and an optional step. */
|
|
77
|
+
const SEQUENCE = /^([+-]?\d+|[A-Za-z])\.\.([+-]?\d+|[A-Za-z])(?:\.\.([+-]?\d+))?$/u;
|
|
78
|
+
/**
|
|
79
|
+
* An integer endpoint with leading zeros, which Bash 4.0 pads to. A `+` sign
|
|
80
|
+
* does not count: Bash 5.3 expands `{+01..3}` as `1 2 3`, as 3.2 does.
|
|
81
|
+
*/
|
|
82
|
+
const ZERO_PADDED = /^-?0\d+$/u;
|
|
83
|
+
/** An integer endpoint. */
|
|
84
|
+
const INTEGER = /^[+-]?\d+$/u;
|
|
85
|
+
/** The signed 64-bit range Bash 5.3 accepts for an endpoint. */
|
|
86
|
+
const INT64_MIN = -(2n ** 63n);
|
|
87
|
+
const INT64_MAX = 2n ** 63n - 1n;
|
|
88
|
+
/**
|
|
89
|
+
* `text` with every backslash escape (the backslash and the character it
|
|
90
|
+
* quotes) replaced by `\0`, which no sequence contains, so an escaped brace
|
|
91
|
+
* cannot open or close a sequence and an escape inside one disqualifies it.
|
|
92
|
+
*/
|
|
93
|
+
function maskEscapes(text) {
|
|
94
|
+
let masked = '';
|
|
95
|
+
for (let index = 0; index < text.length; index++) {
|
|
96
|
+
if (text[index] === '\\') {
|
|
97
|
+
masked += '\0\0';
|
|
98
|
+
index++;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
masked += text[index];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return masked;
|
|
105
|
+
}
|
|
106
|
+
/** The value of an endpoint: an integer's number, or a letter's code point. */
|
|
107
|
+
function endpointValue(endpoint) {
|
|
108
|
+
return INTEGER.test(endpoint) ? BigInt(endpoint) : BigInt(endpoint.charCodeAt(0));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The number of words a valid sequence yields. A step of zero counts as 1,
|
|
112
|
+
* and a step's sign is ignored: the direction follows the endpoints.
|
|
113
|
+
*/
|
|
114
|
+
function resultCount(start, end, step) {
|
|
115
|
+
const size = step === undefined || step === 0n ? 1n : step < 0n ? -step : step;
|
|
116
|
+
return span(start, end) / size + 1n;
|
|
117
|
+
}
|
|
118
|
+
/** The distance between two endpoints. */
|
|
119
|
+
function span(start, end) {
|
|
120
|
+
return end > start ? end - start : start - end;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The first brace sequence in the literal text `text` that needs Bash 4.0
|
|
124
|
+
* and that Bash 5.3 expands, or `undefined`. Innermost brace pairs are the
|
|
125
|
+
* only candidates, which is where Bash finds a sequence, including one
|
|
126
|
+
* nested in a comma list (`{x,{01..03}}`).
|
|
127
|
+
*/
|
|
128
|
+
function findSequence(text) {
|
|
129
|
+
const masked = maskEscapes(text);
|
|
130
|
+
for (const pair of masked.matchAll(/\{([^{}]*)\}/gu)) {
|
|
131
|
+
const match = SEQUENCE.exec(pair[1]);
|
|
132
|
+
if (match === null)
|
|
133
|
+
continue;
|
|
134
|
+
const [, start, end] = match;
|
|
135
|
+
const integers = INTEGER.test(start) && INTEGER.test(end);
|
|
136
|
+
const letters = !INTEGER.test(start) && !INTEGER.test(end);
|
|
137
|
+
if (!integers && !letters)
|
|
138
|
+
continue;
|
|
139
|
+
// A third `..` field is the step (SEQUENCE has already checked its form).
|
|
140
|
+
const stepText = match[0].split('..').length === 3 ? match[0].split('..')[2] : undefined;
|
|
141
|
+
const step = stepText === undefined ? undefined : BigInt(stepText);
|
|
142
|
+
const first = endpointValue(start);
|
|
143
|
+
const last = endpointValue(end);
|
|
144
|
+
const inRange = first >= INT64_MIN &&
|
|
145
|
+
first <= INT64_MAX &&
|
|
146
|
+
last >= INT64_MIN &&
|
|
147
|
+
last <= INT64_MAX &&
|
|
148
|
+
(step === undefined || (step > INT64_MIN && step <= INT64_MAX));
|
|
149
|
+
// The distance between the endpoints must fit too:
|
|
150
|
+
// `{-1..9223372036854775807..9223372036854775807}` stays literal on Bash
|
|
151
|
+
// 5.3 although every number in it is in range.
|
|
152
|
+
if (!inRange || span(first, last) > INT64_MAX)
|
|
153
|
+
continue;
|
|
154
|
+
const results = resultCount(first, last, step);
|
|
155
|
+
const written = text.slice(pair.index, pair.index + pair[0].length);
|
|
156
|
+
if (step !== undefined)
|
|
157
|
+
return { text: written, kind: 'stepped', results, first };
|
|
158
|
+
if (integers && (ZERO_PADDED.test(start) || ZERO_PADDED.test(end))) {
|
|
159
|
+
return { text: written, kind: 'padded', results, first };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
/** How many unescaped `{` the literal parts of `word` hold. */
|
|
165
|
+
function braceGroupCount(word) {
|
|
166
|
+
let count = 0;
|
|
167
|
+
for (const part of word.parts) {
|
|
168
|
+
if (part.type === 'Lit')
|
|
169
|
+
count += maskEscapes(part.value).split('{').length - 1;
|
|
170
|
+
}
|
|
171
|
+
return count;
|
|
172
|
+
}
|
|
173
|
+
/** Redirection operators whose word the shell does not brace-expand. */
|
|
174
|
+
const NO_BRACE_REDIRECTS = new Set([Hdoc, DashHdoc, WordHdoc]);
|
|
175
|
+
/** The `<&` and `>&` operators, whose lone-number word names a descriptor. */
|
|
176
|
+
const DESCRIPTOR_DUPLICATES = new Set([DplIn, DplOut]);
|
|
177
|
+
/**
|
|
178
|
+
* Whether a one-word sequence that is the whole target of `<&` or `>&`
|
|
179
|
+
* behaves the same on Bash 3.2 and newer (checked on 3.2.57 and 5.3.9):
|
|
180
|
+
*
|
|
181
|
+
* - a descriptor position (`<&`, or `>&` with a descriptor before it, as in
|
|
182
|
+
* `2>&`) with a negative number is an "ambiguous redirect" error on both,
|
|
183
|
+
* whatever the form, so there is nothing to port;
|
|
184
|
+
* - a padded non-negative number names the same descriptor on both (`>&{01..01}`
|
|
185
|
+
* is descriptor 1), but a stepped one differs (`>&{1..3..5}` is literal on
|
|
186
|
+
* 3.2 and descriptor 1 on 5.3);
|
|
187
|
+
* - a bare `>&` with a word that is not a descriptor writes a file, so a
|
|
188
|
+
* negative number is a file name that differs (`-1` on 3.2, `-01` on 5.3).
|
|
189
|
+
*/
|
|
190
|
+
function isSameOnEveryBash(redirect, sequence) {
|
|
191
|
+
if (sequence.results !== 1n)
|
|
192
|
+
return false;
|
|
193
|
+
const descriptorPosition = redirect.op === DplIn || redirect.n !== undefined;
|
|
194
|
+
if (sequence.first < 0n)
|
|
195
|
+
return descriptorPosition;
|
|
196
|
+
return sequence.kind === 'padded';
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Whether the shell brace-expands `word`, a `Word` node whose parent is
|
|
200
|
+
* `parent`. This is the complete list of such positions; anything not named
|
|
201
|
+
* here is exempt.
|
|
202
|
+
*/
|
|
203
|
+
function isBraceExpanded(word, parent, grandparent) {
|
|
204
|
+
switch (parent?.type) {
|
|
205
|
+
case 'CallExpr':
|
|
206
|
+
case 'WordIter':
|
|
207
|
+
return true;
|
|
208
|
+
// An array literal element; a keyed element (`[k]=v`) is assigned like a
|
|
209
|
+
// variable and is not expanded.
|
|
210
|
+
case 'ArrayElem':
|
|
211
|
+
return parent.index === undefined && parent.value === word;
|
|
212
|
+
// A declaration's operands are arguments to the builtin, so both a bare
|
|
213
|
+
// one and the value of `NAME=value` are expanded; a plain assignment is
|
|
214
|
+
// not.
|
|
215
|
+
case 'Assign':
|
|
216
|
+
return grandparent?.type === 'DeclClause';
|
|
217
|
+
// The target of an ordinary redirection; a here-document's delimiter and
|
|
218
|
+
// a here-string's word are not brace-expanded.
|
|
219
|
+
case 'Redirect':
|
|
220
|
+
return parent.word === word && !NO_BRACE_REDIRECTS.has(parent.op);
|
|
221
|
+
default:
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The `sh/compat/no-brace-sequence-padding-step` rule module.
|
|
227
|
+
*
|
|
228
|
+
* @internal
|
|
229
|
+
*/
|
|
230
|
+
export const noBraceSequencePaddingStep = createCompatRule({
|
|
231
|
+
name: 'compat/no-brace-sequence-padding-step',
|
|
232
|
+
feature: 'braceSequencePaddingStep',
|
|
233
|
+
meta: {
|
|
234
|
+
type: 'problem',
|
|
235
|
+
docs: {
|
|
236
|
+
description: 'Disallow zero-padded (`{01..10}`) and stepped (`{1..10..2}`) brace sequences when the target shell lacks them',
|
|
237
|
+
},
|
|
238
|
+
messages: {
|
|
239
|
+
padded: '`{{sequence}}` needs {{required}}, but this file targets {{target}}, which expands it without the zero padding. Count in a loop and format with `printf` instead, for example `i=1; while [ "$i" -le 3 ]; do printf \'%02d\\n\' "$i"; i=$((i + 1)); done`.',
|
|
240
|
+
stepped: '`{{sequence}}` needs {{required}}, but this file targets {{target}}, which leaves it as that literal word. Count in a loop instead, for example `i=1; while [ "$i" -le 9 ]; do echo "$i"; i=$((i + 4)); done`.',
|
|
241
|
+
posixPadded: '`{{sequence}}` needs {{required}}, but this file targets {{target}}, which does not specify brace expansion, so what a shell does with this word depends on the shell. Count in a loop and format with `printf` instead, for example `i=1; while [ "$i" -le 3 ]; do printf \'%02d\\n\' "$i"; i=$((i + 1)); done`.',
|
|
242
|
+
posixStepped: '`{{sequence}}` needs {{required}}, but this file targets {{target}}, which does not specify brace expansion, so what a shell does with this word depends on the shell. Count in a loop instead, for example `i=1; while [ "$i" -le 9 ]; do echo "$i"; i=$((i + 4)); done`.',
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
create(context, gate) {
|
|
246
|
+
const { sourceCode } = context;
|
|
247
|
+
const posix = sourceCode.dialect === 'posix';
|
|
248
|
+
return {
|
|
249
|
+
Word(node) {
|
|
250
|
+
const parent = sourceCode.getParent(node);
|
|
251
|
+
const grandparent = parent === undefined ? undefined : sourceCode.getParent(parent);
|
|
252
|
+
if (!isBraceExpanded(node, parent, grandparent))
|
|
253
|
+
return;
|
|
254
|
+
const redirectTarget = parent?.type === 'Redirect';
|
|
255
|
+
for (const part of node.parts) {
|
|
256
|
+
if (part.type !== 'Lit')
|
|
257
|
+
continue;
|
|
258
|
+
const sequence = findSequence(part.value);
|
|
259
|
+
if (sequence === undefined)
|
|
260
|
+
continue;
|
|
261
|
+
// A redirection target is an ambiguous-redirect error unless it is
|
|
262
|
+
// exactly one word, which is only certain for a lone sequence.
|
|
263
|
+
if (redirectTarget && (sequence.results !== 1n || braceGroupCount(node) !== 1))
|
|
264
|
+
return;
|
|
265
|
+
if (!posix &&
|
|
266
|
+
parent?.type === 'Redirect' &&
|
|
267
|
+
DESCRIPTOR_DUPLICATES.has(parent.op) &&
|
|
268
|
+
node.parts.length === 1 &&
|
|
269
|
+
part.value === sequence.text &&
|
|
270
|
+
isSameOnEveryBash(parent, sequence)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
context.report({
|
|
274
|
+
node,
|
|
275
|
+
messageId: posix
|
|
276
|
+
? sequence.kind === 'padded'
|
|
277
|
+
? 'posixPadded'
|
|
278
|
+
: 'posixStepped'
|
|
279
|
+
: sequence.kind,
|
|
280
|
+
data: { ...gate, sequence: sequence.text },
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export type NoCaseAttributesMessageId = 'caseAttribute' | 'caseAttributesBoth';
|
|
7
|
+
/**
|
|
8
|
+
* The `sh/compat/no-case-attributes` rule module.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const noCaseAttributes: import("../define-shell-rule.js").ShellRule<[], NoCaseAttributesMessageId>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-case-attributes`: reports the case-converting attributes
|
|
3
|
+
* `declare -l` and `declare -u` (also through `typeset` and `local`) in files
|
|
4
|
+
* whose target shell lacks them, that is, Bash older than 4.0 (macOS's stock
|
|
5
|
+
* `/bin/bash` is 3.2) and POSIX sh.
|
|
6
|
+
*
|
|
7
|
+
* Bash 3.2 rejects the option, so the declaration fails and the variable is
|
|
8
|
+
* left unset. The message suggests the portable alternative: convert the
|
|
9
|
+
* value with `tr` when assigning it.
|
|
10
|
+
*
|
|
11
|
+
* What is read: `declare`/`typeset`/`local` option clusters containing `l`
|
|
12
|
+
* or `u` (`-l`, `-u`, `-lx`), found as `declarationFlagVisitor` defines. The
|
|
13
|
+
* report is on the option word. A cluster with both letters (`-lu`) reports
|
|
14
|
+
* once, with its own message: on every Bash that has the attributes, `-l`
|
|
15
|
+
* and `-u` together cancel out and leave the value as written (observed on
|
|
16
|
+
* Bash 4.0 and 5.3), so no `tr` conversion matches it.
|
|
17
|
+
*/
|
|
18
|
+
import { createCompatRule } from './create-compat-rule.js';
|
|
19
|
+
import { DECLARATION_AND_LOCAL_COMMANDS, declarationFlagVisitor } from './declaration-options.js';
|
|
20
|
+
/** The `tr` conversion that gives each attribute's result. */
|
|
21
|
+
const TR_FOR = {
|
|
22
|
+
l: "tr '[:upper:]' '[:lower:]'",
|
|
23
|
+
u: "tr '[:lower:]' '[:upper:]'",
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The `sh/compat/no-case-attributes` rule module.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export const noCaseAttributes = createCompatRule({
|
|
31
|
+
name: 'compat/no-case-attributes',
|
|
32
|
+
feature: 'caseAttributes',
|
|
33
|
+
meta: {
|
|
34
|
+
type: 'problem',
|
|
35
|
+
docs: {
|
|
36
|
+
description: 'Disallow the case attributes `declare -l` and `declare -u` when the target shell lacks them',
|
|
37
|
+
},
|
|
38
|
+
messages: {
|
|
39
|
+
caseAttribute: '`{{command}} -{{flag}}` needs {{required}}, but this file targets {{target}}. Convert the value when you assign it instead, for example `v=$(printf \'%s\' "$v" | {{tr}})`.',
|
|
40
|
+
caseAttributesBoth: '`{{command}} {{option}}` needs {{required}}, but this file targets {{target}}. Where it is supported, `-l` and `-u` together cancel out and leave the value as written, so drop both letters.',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
create(context, gate) {
|
|
44
|
+
const { sourceCode } = context;
|
|
45
|
+
return declarationFlagVisitor(sourceCode, '[lu]', (node, command) => {
|
|
46
|
+
const option = (node.type === 'Word' ? sourceCode.resolveWordText(node) : null) ?? '';
|
|
47
|
+
if (option.includes('l') && option.includes('u')) {
|
|
48
|
+
context.report({
|
|
49
|
+
node,
|
|
50
|
+
messageId: 'caseAttributesBoth',
|
|
51
|
+
data: { ...gate, command, option },
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const flag = option.includes('l') ? 'l' : 'u';
|
|
56
|
+
context.report({
|
|
57
|
+
node,
|
|
58
|
+
messageId: 'caseAttribute',
|
|
59
|
+
data: { ...gate, command, flag, tr: TR_FOR[flag] },
|
|
60
|
+
});
|
|
61
|
+
}, DECLARATION_AND_LOCAL_COMMANDS);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export type NoCaseFallthroughMessageId = 'fallthrough' | 'resume';
|
|
7
|
+
/**
|
|
8
|
+
* The `sh/compat/no-case-fallthrough` rule module.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const noCaseFallthrough: import("../define-shell-rule.js").ShellRule<[], NoCaseFallthroughMessageId>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-case-fallthrough`: reports the `case` terminators `;&` (run
|
|
3
|
+
* the next branch without testing its pattern) and `;;&` (go on testing the
|
|
4
|
+
* next patterns) in files whose target shell lacks them, that is, Bash older
|
|
5
|
+
* than 4.0 (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
|
|
6
|
+
*
|
|
7
|
+
* Bash 3.2 rejects both as a syntax error, so the whole script fails to run.
|
|
8
|
+
* POSIX.1-2024 adds `;&`, but dash 0.5.12 still rejects it, so a POSIX
|
|
9
|
+
* target is treated as lacking it. The message suggests the portable
|
|
10
|
+
* rewrite: for `;&`, repeat the next branch's commands at the end of this
|
|
11
|
+
* one and end it with `;;`; for `;;&`, move the patterns after this branch
|
|
12
|
+
* into a second `case` statement.
|
|
13
|
+
*
|
|
14
|
+
* What is read: a `case` branch (`CaseItem`) whose terminator is `;&` or
|
|
15
|
+
* `;;&`. The usual `;;`, and these characters inside quotes or a pattern,
|
|
16
|
+
* are not reported. The report is on the terminator.
|
|
17
|
+
*/
|
|
18
|
+
import { Fallthrough, Resume } from 'sh-ast';
|
|
19
|
+
import { createCompatRule } from './create-compat-rule.js';
|
|
20
|
+
import { operatorRange } from './operator-range.js';
|
|
21
|
+
/** Each terminator's message and spelling. */
|
|
22
|
+
const TERMINATORS = new Map([
|
|
23
|
+
[Fallthrough, { messageId: 'fallthrough', text: ';&' }],
|
|
24
|
+
[Resume, { messageId: 'resume', text: ';;&' }],
|
|
25
|
+
]);
|
|
26
|
+
/**
|
|
27
|
+
* The `sh/compat/no-case-fallthrough` rule module.
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export const noCaseFallthrough = createCompatRule({
|
|
32
|
+
name: 'compat/no-case-fallthrough',
|
|
33
|
+
feature: 'caseFallthrough',
|
|
34
|
+
meta: {
|
|
35
|
+
type: 'problem',
|
|
36
|
+
docs: {
|
|
37
|
+
description: 'Disallow the `case` terminators `;&` and `;;&` when the target shell lacks them',
|
|
38
|
+
},
|
|
39
|
+
messages: {
|
|
40
|
+
fallthrough: "`;&` needs {{required}}, but this file targets {{target}}. Repeat the next branch's commands at the end of this branch and end it with `;;` instead.",
|
|
41
|
+
resume: '`;;&` needs {{required}}, but this file targets {{target}}. End this branch with `;;` and test the patterns that follow it in a second `case` statement instead.',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
create(context, gate) {
|
|
45
|
+
const { sourceCode } = context;
|
|
46
|
+
return {
|
|
47
|
+
CaseItem(node) {
|
|
48
|
+
const terminator = TERMINATORS.get(node.op);
|
|
49
|
+
if (terminator === undefined)
|
|
50
|
+
return;
|
|
51
|
+
const range = operatorRange(sourceCode, node, terminator.text);
|
|
52
|
+
if (range === undefined) {
|
|
53
|
+
context.report({ node, messageId: terminator.messageId, data: { ...gate } });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
context.report({
|
|
57
|
+
loc: {
|
|
58
|
+
start: sourceCode.getLocFromIndex(range[0]),
|
|
59
|
+
end: sourceCode.getLocFromIndex(range[1]),
|
|
60
|
+
},
|
|
61
|
+
messageId: terminator.messageId,
|
|
62
|
+
data: { ...gate },
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export type NoCaseModificationMessageId = 'caseModification' | 'caseModificationPattern';
|
|
7
|
+
/**
|
|
8
|
+
* What `tr` is given to convert, and to which case, for one operator.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export interface CaseOperator {
|
|
13
|
+
/** The operator as written after the name. */
|
|
14
|
+
readonly operator: string;
|
|
15
|
+
/** Whether the operator converts every character (`^^`, `,,`) or the first. */
|
|
16
|
+
readonly all: boolean;
|
|
17
|
+
/** `tr` arguments that perform the conversion. */
|
|
18
|
+
readonly tr: string;
|
|
19
|
+
}
|
|
20
|
+
/** `tr` arguments that convert to upper case. @internal */
|
|
21
|
+
export declare const TO_UPPER = "tr '[:lower:]' '[:upper:]'";
|
|
22
|
+
/** `tr` arguments that convert to lower case. @internal */
|
|
23
|
+
export declare const TO_LOWER = "tr '[:upper:]' '[:lower:]'";
|
|
24
|
+
/**
|
|
25
|
+
* The `tr` replacement shown in the message for `name` under `spec`.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export declare function trExample(name: string, spec: CaseOperator): string;
|
|
30
|
+
/**
|
|
31
|
+
* The `sh/compat/no-case-modification` rule module.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export declare const noCaseModification: import("../define-shell-rule.js").ShellRule<[], NoCaseModificationMessageId>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-case-modification`: reports the case-modifying parameter
|
|
3
|
+
* expansions `${v^}`, `${v^^}`, `${v,}` and `${v,,}` (with or without a
|
|
4
|
+
* pattern) in files whose target shell lacks them, that is, Bash older than
|
|
5
|
+
* 4.0 (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
|
|
6
|
+
*
|
|
7
|
+
* Bash 3.2 rejects the expansion with "bad substitution" at the moment the
|
|
8
|
+
* word is expanded. The message suggests the portable alternative: pipe the
|
|
9
|
+
* value through `tr`. The example it shows is for the operator written: all
|
|
10
|
+
* characters for `^^` and `,,`, the first character for `^` and `,`
|
|
11
|
+
* (`${v%"${v#?}"}` is the first character and `${v#?}` the rest).
|
|
12
|
+
*
|
|
13
|
+
* A pattern form (`${v^^a}`) changes only the characters the pattern matches,
|
|
14
|
+
* which a plain case-converting `tr` does not reproduce, so it gets a message
|
|
15
|
+
* that offers no `tr` equivalent.
|
|
16
|
+
*
|
|
17
|
+
* What is read: a `ParamExp` whose operator is one of the four case
|
|
18
|
+
* operators. The report is on the whole expansion.
|
|
19
|
+
*/
|
|
20
|
+
import { UpperAll, UpperFirst, LowerAll, LowerFirst } from 'sh-ast';
|
|
21
|
+
import { createCompatRule } from './create-compat-rule.js';
|
|
22
|
+
/** `tr` arguments that convert to upper case. @internal */
|
|
23
|
+
export const TO_UPPER = "tr '[:lower:]' '[:upper:]'";
|
|
24
|
+
/** `tr` arguments that convert to lower case. @internal */
|
|
25
|
+
export const TO_LOWER = "tr '[:upper:]' '[:lower:]'";
|
|
26
|
+
/** The four case operators, keyed by the parser's operator constant. */
|
|
27
|
+
const CASE_OPERATORS = new Map([
|
|
28
|
+
[UpperAll, { operator: '^^', all: true, tr: TO_UPPER }],
|
|
29
|
+
[UpperFirst, { operator: '^', all: false, tr: TO_UPPER }],
|
|
30
|
+
[LowerAll, { operator: ',,', all: true, tr: TO_LOWER }],
|
|
31
|
+
[LowerFirst, { operator: ',', all: false, tr: TO_LOWER }],
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* The `tr` replacement shown in the message for `name` under `spec`.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export function trExample(name, spec) {
|
|
39
|
+
const whole = `"$${name}"`;
|
|
40
|
+
return spec.all
|
|
41
|
+
? `$(printf '%s' ${whole} | ${spec.tr})`
|
|
42
|
+
: `$(printf '%s' "\${${name}%"\${${name}#?}"}" | ${spec.tr})"\${${name}#?}"`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The `sh/compat/no-case-modification` rule module.
|
|
46
|
+
*
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
export const noCaseModification = createCompatRule({
|
|
50
|
+
name: 'compat/no-case-modification',
|
|
51
|
+
feature: 'caseModification',
|
|
52
|
+
meta: {
|
|
53
|
+
type: 'problem',
|
|
54
|
+
docs: {
|
|
55
|
+
description: 'Disallow case-modifying expansions (`${v^^}`, `${v,,}` and variants) when the target shell lacks them',
|
|
56
|
+
},
|
|
57
|
+
messages: {
|
|
58
|
+
caseModification: '`${…{{operator}}}` needs {{required}}, but this file targets {{target}}. Convert the case with `tr` instead, for example {{example}}.',
|
|
59
|
+
caseModificationPattern: '`${…{{operator}}pattern}` needs {{required}}, but this file targets {{target}}. It changes only the characters the pattern matches, so a plain case-converting `tr` is not equivalent; spell out the matching characters instead (for example `tr a A`).',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
create(context, gate) {
|
|
63
|
+
return {
|
|
64
|
+
ParamExp(node) {
|
|
65
|
+
const { exp } = node;
|
|
66
|
+
const spec = exp === undefined ? undefined : CASE_OPERATORS.get(exp.op);
|
|
67
|
+
if (exp === undefined || spec === undefined)
|
|
68
|
+
return;
|
|
69
|
+
const name = node.param?.value ?? 'v';
|
|
70
|
+
if (exp.word !== undefined) {
|
|
71
|
+
context.report({
|
|
72
|
+
node,
|
|
73
|
+
messageId: 'caseModificationPattern',
|
|
74
|
+
data: { ...gate, operator: spec.operator },
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
context.report({
|
|
79
|
+
node,
|
|
80
|
+
messageId: 'caseModification',
|
|
81
|
+
data: { ...gate, operator: spec.operator, example: trExample(name, spec) },
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export type NoCoprocMessageId = 'coproc';
|
|
7
|
+
/**
|
|
8
|
+
* The `sh/compat/no-coproc` rule module.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const noCoproc: import("../define-shell-rule.js").ShellRule<[], "coproc">;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-coproc`: reports the `coproc` reserved word (a coprocess:
|
|
3
|
+
* a background command with a pipe pair to the shell) in files whose target
|
|
4
|
+
* shell lacks it, that is, Bash older than 4.0 (macOS's stock `/bin/bash` is
|
|
5
|
+
* 3.2) and POSIX sh.
|
|
6
|
+
*
|
|
7
|
+
* Bash 3.2 does not know the word, so `coproc cat` runs an external command
|
|
8
|
+
* named `coproc` and fails with `command not found`. The message suggests the
|
|
9
|
+
* portable alternative: a named pipe made with `mkfifo`, with the producer
|
|
10
|
+
* started in the background.
|
|
11
|
+
*
|
|
12
|
+
* What is read: Bash's `CoprocClause`. Under `dialect: "posix"` the parser
|
|
13
|
+
* has no such clause and reads `coproc cat` as an ordinary command, so a
|
|
14
|
+
* command whose name is `coproc` is reported too.
|
|
15
|
+
*/
|
|
16
|
+
import { createCompatRule } from './create-compat-rule.js';
|
|
17
|
+
/**
|
|
18
|
+
* The `sh/compat/no-coproc` rule module.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export const noCoproc = createCompatRule({
|
|
23
|
+
name: 'compat/no-coproc',
|
|
24
|
+
feature: 'coproc',
|
|
25
|
+
meta: {
|
|
26
|
+
type: 'problem',
|
|
27
|
+
docs: {
|
|
28
|
+
description: 'Disallow the `coproc` reserved word when the target shell lacks it',
|
|
29
|
+
},
|
|
30
|
+
messages: {
|
|
31
|
+
coproc: '`coproc` needs {{required}}, but this file targets {{target}}. Use a named pipe instead: create it with `mkfifo`, start the coprocess in the background reading or writing the pipe, and open the pipe from the script.',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
create(context, gate) {
|
|
35
|
+
const { sourceCode } = context;
|
|
36
|
+
return {
|
|
37
|
+
CoprocClause(node) {
|
|
38
|
+
context.report({ node, messageId: 'coproc', data: { ...gate } });
|
|
39
|
+
},
|
|
40
|
+
CallExpr(node) {
|
|
41
|
+
if (sourceCode.getCommandName(node) === 'coproc') {
|
|
42
|
+
context.report({ node, messageId: 'coproc', data: { ...gate } });
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export type NoDeclareGMessageId = 'declareGlobal' | 'declareGlobalWithAttributes';
|
|
7
|
+
/**
|
|
8
|
+
* The `sh/compat/no-declare-g` rule module.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const noDeclareG: import("../define-shell-rule.js").ShellRule<[], NoDeclareGMessageId>;
|