@eslint-sh/eslint-plugin 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -8
- package/dist/command-harvest/builtins/bash.d.ts +1 -1
- package/dist/command-harvest/builtins/bash.js +1 -1
- package/dist/command-harvest/builtins/bats.d.ts +2 -0
- package/dist/command-harvest/builtins/bats.js +3 -0
- package/dist/command-harvest/wrapper-table.d.ts +3 -19
- package/dist/command-harvest/wrapper-table.js +8 -38
- package/dist/configs/bash-3-2.d.ts +7 -6
- package/dist/configs/bash-3-2.js +5 -3
- package/dist/configs/posix-portable.d.ts +1 -1
- package/dist/configs/posix-portable.js +1 -0
- package/dist/declaration-conventions/read-declared-commands.d.ts +2 -2
- package/dist/declaration-conventions/read-declared-commands.js +2 -2
- package/dist/eslint-plugin-syntax.d.ts +4 -0
- package/dist/eslint-plugin.d.ts +86 -40
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/language/shell-language-options.d.ts +18 -15
- package/dist/language/shell-language-options.js +33 -26
- package/dist/language/shell-language.d.ts +10 -9
- package/dist/language/shell-language.js +18 -35
- package/dist/language/shell-source-code.d.ts +17 -9
- package/dist/language/shell-source-code.js +53 -12
- package/dist/rules/bash-variable-semantics.d.ts +22 -0
- package/dist/rules/bash-variable-semantics.js +41 -0
- package/dist/rules/compat/create-compat-rule.js +1 -1
- package/dist/rules/compat/declaration-options.d.ts +2 -2
- package/dist/rules/compat/declaration-options.js +5 -2
- package/dist/rules/compat/no-append-all-redirect.js +1 -1
- package/dist/rules/compat/no-associative-arrays.js +2 -2
- 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.js +3 -3
- package/dist/rules/compat/no-case-fallthrough.js +2 -2
- package/dist/rules/compat/no-case-modification.js +2 -2
- package/dist/rules/compat/no-coproc.js +1 -1
- package/dist/rules/compat/no-declare-g.js +3 -3
- package/dist/rules/compat/no-double-bracket.js +2 -2
- package/dist/rules/compat/no-epoch-variables.js +2 -2
- package/dist/rules/compat/no-fd-variable.js +1 -1
- package/dist/rules/compat/no-globstar.js +1 -1
- package/dist/rules/compat/no-lastpipe-semantics.js +2 -2
- package/dist/rules/compat/no-let.js +2 -1
- package/dist/rules/compat/no-mapfile.js +4 -2
- package/dist/rules/compat/no-nameref.js +3 -3
- package/dist/rules/compat/no-negative-array-index.js +39 -9
- package/dist/rules/compat/no-nounset-empty-array.js +15 -4
- package/dist/rules/compat/no-parameter-transformation.js +2 -2
- package/dist/rules/compat/no-pipe-all.js +1 -1
- package/dist/rules/compat/no-printf-time-format.js +4 -2
- package/dist/rules/compat/no-source.js +2 -1
- package/dist/rules/compat/no-test-v.js +4 -2
- package/dist/rules/compat/no-unicode-escape.js +2 -2
- package/dist/rules/compat/no-wait-n.js +4 -2
- package/dist/rules/compat/nounset-array-proof.d.ts +8 -0
- package/dist/rules/compat/nounset-array-proof.js +563 -0
- package/dist/rules/compat/posix-absent-command.d.ts +7 -8
- package/dist/rules/compat/posix-absent-command.js +17 -15
- package/dist/rules/compat/shopt-options.js +3 -1
- package/dist/rules/compat/version-gates.d.ts +10 -10
- package/dist/rules/compat/version-gates.js +21 -13
- package/dist/rules/create-wrapper-rule.js +12 -7
- package/dist/rules/index.js +2 -0
- package/dist/rules/no-dynamic-invocation.d.ts +0 -8
- package/dist/rules/no-dynamic-invocation.js +7 -15
- package/dist/rules/no-eval.js +3 -1
- package/dist/rules/no-masked-exit-status.js +15 -8
- package/dist/rules/no-secret-env-reads.d.ts +4 -3
- package/dist/rules/no-secret-env-reads.js +37 -14
- package/dist/rules/no-secret-flags.d.ts +2 -1
- package/dist/rules/no-secret-flags.js +7 -8
- package/dist/rules/no-unchecked-cd.js +3 -1
- package/dist/rules/no-undeclared-commands.d.ts +1 -15
- package/dist/rules/no-undeclared-commands.js +3 -15
- package/dist/rules/no-unused-declarations.d.ts +1 -18
- package/dist/rules/no-unused-declarations.js +9 -34
- package/dist/rules/prefer-local.js +4 -3
- package/dist/rules/quote-expansions.js +3 -28
- package/dist/rules/require-network-wrapper.d.ts +2 -17
- package/dist/rules/require-network-wrapper.js +33 -4
- package/dist/rules/require-tty-guard.js +30 -1
- package/dist/rules/resolve-declaration-convention.d.ts +5 -6
- package/dist/rules/resolve-declaration-convention.js +5 -6
- package/dist/rules/resolve-network-wrapper.d.ts +19 -0
- package/dist/rules/resolve-network-wrapper.js +57 -26
- package/dist/rules/resolve-transparent-wrappers.d.ts +43 -0
- package/dist/rules/resolve-transparent-wrappers.js +98 -0
- package/dist/rules/set-options.d.ts +2 -1
- package/dist/rules/set-options.js +17 -5
- package/dist/rules/shebang-interpreter.js +4 -2
- package/dist/rules/transparent-wrappers.d.ts +4 -3
- package/dist/rules/transparent-wrappers.js +5 -4
- package/dist/rules/user-pattern.d.ts +5 -3
- package/dist/rules/user-pattern.js +6 -4
- package/dist/syntax/index.d.ts +4 -0
- package/dist/syntax/index.js +4 -0
- package/docs/compat.md +90 -34
- package/docs/custom-rule-tutorial.md +9 -3
- package/docs/examples/no-listed-commands-rule.ts +2 -2
- package/docs/rules/README.md +38 -5
- package/docs/rules/_template.md +2 -2
- package/docs/rules/compat/no-append-all-redirect.md +12 -12
- package/docs/rules/compat/no-associative-arrays.md +14 -14
- package/docs/rules/compat/no-brace-sequence-padding-step.md +90 -0
- package/docs/rules/compat/no-case-attributes.md +13 -13
- package/docs/rules/compat/no-case-fallthrough.md +12 -12
- package/docs/rules/compat/no-case-modification.md +11 -11
- package/docs/rules/compat/no-coproc.md +13 -13
- package/docs/rules/compat/no-declare-g.md +13 -13
- package/docs/rules/compat/no-double-bracket.md +2 -2
- package/docs/rules/compat/no-epoch-variables.md +13 -13
- package/docs/rules/compat/no-fd-variable.md +12 -12
- package/docs/rules/compat/no-globstar.md +13 -13
- package/docs/rules/compat/no-lastpipe-semantics.md +13 -13
- package/docs/rules/compat/no-let.md +2 -2
- package/docs/rules/compat/no-mapfile.md +16 -16
- package/docs/rules/compat/no-nameref.md +13 -13
- package/docs/rules/compat/no-negative-array-index.md +14 -14
- package/docs/rules/compat/no-nounset-empty-array.md +72 -17
- package/docs/rules/compat/no-parameter-transformation.md +19 -19
- package/docs/rules/compat/no-pipe-all.md +13 -13
- package/docs/rules/compat/no-printf-time-format.md +13 -13
- package/docs/rules/compat/no-source.md +2 -2
- package/docs/rules/compat/no-test-v.md +12 -12
- package/docs/rules/compat/no-unicode-escape.md +13 -13
- package/docs/rules/compat/no-wait-n.md +13 -13
- package/docs/rules/no-dynamic-invocation.md +17 -12
- package/docs/rules/no-eval.md +6 -1
- package/docs/rules/no-masked-exit-status.md +4 -0
- package/docs/rules/no-secret-env-reads.md +34 -3
- package/docs/rules/no-secret-flags.md +19 -2
- package/docs/rules/no-unchecked-cd.md +9 -2
- package/docs/rules/no-undeclared-commands.md +16 -15
- package/docs/rules/no-unused-declarations.md +17 -20
- package/docs/rules/prefer-local.md +20 -2
- package/docs/rules/quote-expansions.md +8 -5
- package/docs/rules/require-network-wrapper.md +22 -17
- package/docs/rules/require-shebang.md +10 -4
- package/docs/rules/require-strict-mode.md +6 -0
- package/docs/rules/require-tty-guard.md +4 -1
- package/docs/shellcheck.md +10 -5
- package/docs/transparent-wrappers.md +97 -0
- package/docs/untrusted-code.md +440 -0
- package/docs/wrapper-rules.md +9 -0
- package/package.json +2 -2
- package/dist/language/end-of-input-marker.d.ts +0 -29
- package/dist/language/end-of-input-marker.js +0 -88
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* {@link DECLARATION_AND_LOCAL_COMMANDS}.
|
|
15
15
|
*/
|
|
16
16
|
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
17
|
+
import { resolveTransparentWrappers } from '../resolve-transparent-wrappers.js';
|
|
17
18
|
import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
|
|
18
19
|
/** The declaration commands that take option clusters. */
|
|
19
20
|
const DECLARATION_COMMANDS = new Set(['declare', 'typeset']);
|
|
@@ -78,7 +79,9 @@ function declClauseArgs(node) {
|
|
|
78
79
|
*
|
|
79
80
|
* @internal
|
|
80
81
|
*/
|
|
81
|
-
export function declarationFlagVisitor(
|
|
82
|
+
export function declarationFlagVisitor(context, flag, report, commands = DECLARATION_COMMANDS) {
|
|
83
|
+
const { sourceCode } = context;
|
|
84
|
+
const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
|
|
82
85
|
const flagPattern = new RegExp(`^-[a-zA-Z]*${flag}[a-zA-Z]*$`, 'u');
|
|
83
86
|
return {
|
|
84
87
|
DeclClause(node) {
|
|
@@ -91,7 +94,7 @@ export function declarationFlagVisitor(sourceCode, flag, report, commands = DECL
|
|
|
91
94
|
},
|
|
92
95
|
CallExpr(node) {
|
|
93
96
|
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
94
|
-
transparentWrappers
|
|
97
|
+
transparentWrappers,
|
|
95
98
|
});
|
|
96
99
|
if (invoked?.effective.static !== true)
|
|
97
100
|
return;
|
|
@@ -37,7 +37,7 @@ export const noAppendAllRedirect = createCompatRule({
|
|
|
37
37
|
description: 'Disallow the `&>>` redirection when the target shell lacks it',
|
|
38
38
|
},
|
|
39
39
|
messages: {
|
|
40
|
-
appendAll: '`&>>` needs {{required}}, but this file
|
|
40
|
+
appendAll: '`&>>` needs {{required}}, but this file must support {{target}}. Append both streams with `>> file 2>&1` instead.',
|
|
41
41
|
useAppendRedirects: 'Replace `&>> {{file}}` with `>> {{file}} 2>&1`.',
|
|
42
42
|
},
|
|
43
43
|
},
|
|
@@ -28,11 +28,11 @@ export const noAssociativeArrays = createCompatRule({
|
|
|
28
28
|
description: 'Disallow associative arrays (`declare -A`, `typeset -A`) when the target shell lacks them',
|
|
29
29
|
},
|
|
30
30
|
messages: {
|
|
31
|
-
associativeArray: 'Associative arrays (`{{command}} -A`) need {{required}}, but this file
|
|
31
|
+
associativeArray: 'Associative arrays (`{{command}} -A`) need {{required}}, but this file must support {{target}}. Consider moving the keyed lookup into `jq`, for example `jq -r --arg k "$key" \'.[$k]\' map.json`.',
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
34
|
create(context, gate) {
|
|
35
|
-
return declarationFlagVisitor(context
|
|
35
|
+
return declarationFlagVisitor(context, 'A', (node, command) => {
|
|
36
36
|
context.report({ node, messageId: 'associativeArray', data: { ...gate, command } });
|
|
37
37
|
});
|
|
38
38
|
},
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with: `padded` for a zero-padded sequence on
|
|
3
|
+
* a Bash that expands it without the padding, `stepped` for a stepped one on
|
|
4
|
+
* a Bash that leaves it literal, and `posixPadded` / `posixStepped` for the
|
|
5
|
+
* same two under a POSIX target, where the result depends on the shell.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export type NoBraceSequencePaddingStepMessageId = 'padded' | 'stepped' | 'posixPadded' | 'posixStepped';
|
|
10
|
+
/**
|
|
11
|
+
* The `sh/compat/no-brace-sequence-padding-step` rule module.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export declare const noBraceSequencePaddingStep: import("../define-shell-rule.js").ShellRule<[], NoBraceSequencePaddingStepMessageId>;
|
|
@@ -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 must support {{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 must support {{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 must support {{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 must support {{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
|
+
});
|
|
@@ -36,13 +36,13 @@ export const noCaseAttributes = createCompatRule({
|
|
|
36
36
|
description: 'Disallow the case attributes `declare -l` and `declare -u` when the target shell lacks them',
|
|
37
37
|
},
|
|
38
38
|
messages: {
|
|
39
|
-
caseAttribute: '`{{command}} -{{flag}}` needs {{required}}, but this file
|
|
40
|
-
caseAttributesBoth: '`{{command}} {{option}}` needs {{required}}, but this file
|
|
39
|
+
caseAttribute: '`{{command}} -{{flag}}` needs {{required}}, but this file must support {{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 must support {{target}}. Where it is supported, `-l` and `-u` together cancel out and leave the value as written, so drop both letters.',
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
43
|
create(context, gate) {
|
|
44
44
|
const { sourceCode } = context;
|
|
45
|
-
return declarationFlagVisitor(
|
|
45
|
+
return declarationFlagVisitor(context, '[lu]', (node, command) => {
|
|
46
46
|
const option = (node.type === 'Word' ? sourceCode.resolveWordText(node) : null) ?? '';
|
|
47
47
|
if (option.includes('l') && option.includes('u')) {
|
|
48
48
|
context.report({
|
|
@@ -37,8 +37,8 @@ export const noCaseFallthrough = createCompatRule({
|
|
|
37
37
|
description: 'Disallow the `case` terminators `;&` and `;;&` when the target shell lacks them',
|
|
38
38
|
},
|
|
39
39
|
messages: {
|
|
40
|
-
fallthrough: "`;&` needs {{required}}, but this file
|
|
41
|
-
resume: '`;;&` needs {{required}}, but this file
|
|
40
|
+
fallthrough: "`;&` needs {{required}}, but this file must support {{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 must support {{target}}. End this branch with `;;` and test the patterns that follow it in a second `case` statement instead.',
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
44
|
create(context, gate) {
|
|
@@ -55,8 +55,8 @@ export const noCaseModification = createCompatRule({
|
|
|
55
55
|
description: 'Disallow case-modifying expansions (`${v^^}`, `${v,,}` and variants) when the target shell lacks them',
|
|
56
56
|
},
|
|
57
57
|
messages: {
|
|
58
|
-
caseModification: '`${…{{operator}}}` needs {{required}}, but this file
|
|
59
|
-
caseModificationPattern: '`${…{{operator}}pattern}` needs {{required}}, but this file
|
|
58
|
+
caseModification: '`${…{{operator}}}` needs {{required}}, but this file must support {{target}}. Convert the case with `tr` instead, for example {{example}}.',
|
|
59
|
+
caseModificationPattern: '`${…{{operator}}pattern}` needs {{required}}, but this file must support {{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
60
|
},
|
|
61
61
|
},
|
|
62
62
|
create(context, gate) {
|
|
@@ -28,7 +28,7 @@ export const noCoproc = createCompatRule({
|
|
|
28
28
|
description: 'Disallow the `coproc` reserved word when the target shell lacks it',
|
|
29
29
|
},
|
|
30
30
|
messages: {
|
|
31
|
-
coproc: '`coproc` needs {{required}}, but this file
|
|
31
|
+
coproc: '`coproc` needs {{required}}, but this file must support {{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
32
|
},
|
|
33
33
|
},
|
|
34
34
|
create(context, gate) {
|
|
@@ -39,12 +39,12 @@ export const noDeclareG = createCompatRule({
|
|
|
39
39
|
description: 'Disallow `declare -g` and `typeset -g` when the target shell lacks the option',
|
|
40
40
|
},
|
|
41
41
|
messages: {
|
|
42
|
-
declareGlobal: '`{{command}} -g` needs {{required}}, but this file
|
|
43
|
-
declareGlobalWithAttributes: '`{{command}} {{flags}}` needs {{required}}, but this file
|
|
42
|
+
declareGlobal: '`{{command}} -g` needs {{required}}, but this file must support {{target}}. Assign the global with a plain `name=value` (a variable not declared `local` is global), or `export name=value` to also export it.',
|
|
43
|
+
declareGlobalWithAttributes: '`{{command}} {{flags}}` needs {{required}}, but this file must support {{target}}. A plain `name=value` cannot express its other options, so dropping `g` is not enough: declare the variable outside any function, or raise the minimum Bash version.',
|
|
44
44
|
},
|
|
45
45
|
},
|
|
46
46
|
create(context, gate) {
|
|
47
|
-
return declarationFlagVisitor(context
|
|
47
|
+
return declarationFlagVisitor(context, 'g', (node, command, options) => {
|
|
48
48
|
if (!isPlainAssignmentEquivalent(options)) {
|
|
49
49
|
context.report({
|
|
50
50
|
node,
|
|
@@ -39,10 +39,10 @@ export const noDoubleBracket = createCompatRule({
|
|
|
39
39
|
},
|
|
40
40
|
},
|
|
41
41
|
create(context, gate) {
|
|
42
|
-
const
|
|
42
|
+
const reachableWord = reachableCommandWord(context, '[[');
|
|
43
43
|
return {
|
|
44
44
|
CallExpr(node) {
|
|
45
|
-
const word =
|
|
45
|
+
const word = reachableWord(node);
|
|
46
46
|
if (word === undefined)
|
|
47
47
|
return;
|
|
48
48
|
context.report({ node: word, messageId: 'doubleBracket', data: { target: gate.target } });
|
|
@@ -27,8 +27,8 @@ export const noEpochVariables = createCompatRule({
|
|
|
27
27
|
description: 'Disallow `$EPOCHSECONDS` and `$EPOCHREALTIME` when the target shell lacks them',
|
|
28
28
|
},
|
|
29
29
|
messages: {
|
|
30
|
-
epochSeconds: '`$EPOCHSECONDS` needs {{required}}, but this file
|
|
31
|
-
epochRealtime: '`$EPOCHREALTIME` needs {{required}}, but this file
|
|
30
|
+
epochSeconds: '`$EPOCHSECONDS` needs {{required}}, but this file must support {{target}}, where it is unset. Use `$(date +%s)` instead (GNU, BSD and BusyBox `date`; POSIX `date` has no `%s`).',
|
|
31
|
+
epochRealtime: '`$EPOCHREALTIME` needs {{required}}, but this file must support {{target}}, where it is unset. `$(date +%s)` gives whole seconds (GNU, BSD and BusyBox `date`); there is no portable source of the fraction.',
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
34
|
create(context, gate) {
|
|
@@ -32,7 +32,7 @@ export const noFdVariable = createCompatRule({
|
|
|
32
32
|
description: 'Disallow `{varname}` file-descriptor redirections (`exec {fd}>file`) when the target shell lacks them',
|
|
33
33
|
},
|
|
34
34
|
messages: {
|
|
35
|
-
fdVariable: '`{{{name}}}` descriptor allocation needs {{required}}, but this file
|
|
35
|
+
fdVariable: '`{{{name}}}` descriptor allocation needs {{required}}, but this file must support {{target}}. Use a fixed descriptor number instead, for example `exec 3>file`, then `>&3`, then `exec 3>&-`.',
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
38
|
create(context, gate) {
|
|
@@ -28,7 +28,7 @@ export const noGlobstar = createCompatRule({
|
|
|
28
28
|
description: 'Disallow `shopt -s globstar` when the target shell lacks the option',
|
|
29
29
|
},
|
|
30
30
|
messages: {
|
|
31
|
-
globstar: "`shopt -s globstar` needs {{required}}, but this file
|
|
31
|
+
globstar: "`shopt -s globstar` needs {{required}}, but this file must support {{target}}, where `**` matches like `*`. Search the tree with `find` instead, for example `find dir -name '*.txt'`.",
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
34
|
create(context, gate) {
|
|
@@ -37,8 +37,8 @@ export const noLastpipeSemantics = createCompatRule({
|
|
|
37
37
|
description: 'Disallow `shopt -s lastpipe` when the target shell lacks the option',
|
|
38
38
|
},
|
|
39
39
|
messages: {
|
|
40
|
-
lastpipe: '`shopt -s lastpipe` needs {{required}}, but this file
|
|
41
|
-
lastpipePosix: '`shopt -s lastpipe` needs {{required}}, but this file
|
|
40
|
+
lastpipe: '`shopt -s lastpipe` needs {{required}}, but this file must support {{target}}. Keep the loop in the current shell by feeding it with process substitution instead, for example `while read -r line; do …; done < <(cmd)`.',
|
|
41
|
+
lastpipePosix: '`shopt -s lastpipe` needs {{required}}, but this file must support {{target}}. Process substitution is not POSIX either; keep the loop in the current shell by writing the output to a temporary file and reading that, for example `cmd > "$tmp"` then `while read -r line; do …; done < "$tmp"`, or by feeding the loop a here-document.',
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
44
|
create(context, gate) {
|
|
@@ -47,9 +47,10 @@ export const noLet = createCompatRule({
|
|
|
47
47
|
create(context, gate) {
|
|
48
48
|
const { sourceCode } = context;
|
|
49
49
|
const fileDefinesLet = definedFunctionNames(sourceCode).has(LET);
|
|
50
|
+
const reachableWord = reachableCommandWord(context, LET);
|
|
50
51
|
return {
|
|
51
52
|
CallExpr(node) {
|
|
52
|
-
const word =
|
|
53
|
+
const word = reachableWord(node);
|
|
53
54
|
if (word === undefined)
|
|
54
55
|
return;
|
|
55
56
|
// Only a name written directly can run a function: a wrapper skips it.
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* all, get the report without a suggestion.
|
|
29
29
|
*/
|
|
30
30
|
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
31
|
+
import { resolveTransparentWrappers } from '../resolve-transparent-wrappers.js';
|
|
31
32
|
import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
|
|
32
33
|
import { createCompatRule } from './create-compat-rule.js';
|
|
33
34
|
/** The builtin's two names. */
|
|
@@ -84,17 +85,18 @@ export const noMapfile = createCompatRule({
|
|
|
84
85
|
description: 'Disallow `mapfile` and `readarray` when the target shell lacks them',
|
|
85
86
|
},
|
|
86
87
|
messages: {
|
|
87
|
-
mapfile: '`{{command}}` needs {{required}}, but this file
|
|
88
|
+
mapfile: '`{{command}}` needs {{required}}, but this file must support {{target}}. Read the lines with a `while IFS= read -r` loop instead.',
|
|
88
89
|
useReadLoop: 'Replace with a `while IFS= read -r` loop that appends each line to `{{array}}` (the loop also sets `REPLY` to the empty string).',
|
|
89
90
|
},
|
|
90
91
|
},
|
|
91
92
|
create(context, gate) {
|
|
92
93
|
const { sourceCode } = context;
|
|
94
|
+
const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
|
|
93
95
|
const arraysAvailable = sourceCode.dialect !== 'posix';
|
|
94
96
|
return {
|
|
95
97
|
CallExpr(node) {
|
|
96
98
|
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
97
|
-
transparentWrappers
|
|
99
|
+
transparentWrappers,
|
|
98
100
|
});
|
|
99
101
|
if (invoked?.effective.static !== true)
|
|
100
102
|
return;
|
|
@@ -30,14 +30,14 @@ export const noNameref = createCompatRule({
|
|
|
30
30
|
description: 'Disallow namerefs (`declare -n`, `local -n`) when the target shell lacks them',
|
|
31
31
|
},
|
|
32
32
|
messages: {
|
|
33
|
-
nameref: '`{{command}} -n` needs {{required}}, but this file
|
|
34
|
-
namerefPosix: '`{{command}} -n` needs {{required}}, but this file
|
|
33
|
+
nameref: '`{{command}} -n` needs {{required}}, but this file must support {{target}}. Pass the variable name instead: assign through it with `printf -v "$name" \'%s\' "$value"` and read it with `${!name}`.',
|
|
34
|
+
namerefPosix: '`{{command}} -n` needs {{required}}, but this file must support {{target}}. Pass the variable name instead and use `eval`: assign with `eval "$name=\\$value"` and read with `eval "value=\\$$name"`, after checking that the name is a valid identifier.',
|
|
35
35
|
},
|
|
36
36
|
},
|
|
37
37
|
create(context, gate) {
|
|
38
38
|
const { sourceCode } = context;
|
|
39
39
|
const messageId = sourceCode.dialect === 'posix' ? 'namerefPosix' : 'nameref';
|
|
40
|
-
return declarationFlagVisitor(
|
|
40
|
+
return declarationFlagVisitor(context, 'n', (node, command) => {
|
|
41
41
|
context.report({ node, messageId, data: { ...gate, command } });
|
|
42
42
|
}, DECLARATION_AND_LOCAL_COMMANDS);
|
|
43
43
|
},
|