@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
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
* so the two agree only for arrays without holes; the suggestion message says
|
|
16
16
|
* so. Assignments and other expansion forms are reported without a suggestion.
|
|
17
17
|
*
|
|
18
|
-
* What is read: an `Index` that is a unary minus applied to a static
|
|
19
|
-
* integer, on a `ParamExp` or an `Assign`.
|
|
18
|
+
* What is read: an `Index` that is a unary minus applied to a static
|
|
19
|
+
* integer (decimal, octal, hexadecimal or base#digits), on a `ParamExp` or an `Assign`.
|
|
20
|
+
* A computed subscript such as
|
|
20
21
|
* `-$n` is not a literal and is not reported, and neither is `-0`, which is
|
|
21
22
|
* zero. A `ParamExp` or `Assign` subscript is read from the syntax tree, so
|
|
22
23
|
* its operand can be any arithmetic node (`-(1)` holds a parenthesized one);
|
|
23
|
-
* only a plain word is resolved.
|
|
24
|
+
* only a plain word is resolved. Arithmetic expressions are never evaluated.
|
|
24
25
|
*
|
|
25
26
|
* Unsetting an element (`unset arr[-1]`, quoted or not) is reported from the
|
|
26
27
|
* `unset` command's operand text, since the parser keeps that operand as an
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
*/
|
|
29
30
|
import { Minus } from 'sh-ast';
|
|
30
31
|
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
32
|
+
import { resolveTransparentWrappers } from '../resolve-transparent-wrappers.js';
|
|
31
33
|
import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
|
|
32
34
|
import { createCompatRule } from './create-compat-rule.js';
|
|
33
35
|
/** A bare negative-subscript read, the only shape the suggestion rewrites. */
|
|
@@ -41,7 +43,33 @@ const TRANSPARENT_WRAPPERS = [
|
|
|
41
43
|
BUILTIN_WRAPPER,
|
|
42
44
|
];
|
|
43
45
|
/** An `unset` operand naming an element by a literal negative subscript. */
|
|
44
|
-
const UNSET_NEGATIVE = /^[A-Za-z_][A-Za-z0-9_]*\[\s*-\s
|
|
46
|
+
const UNSET_NEGATIVE = /^[A-Za-z_][A-Za-z0-9_]*\[\s*-\s*([^\s\]]+)\s*\]$/u;
|
|
47
|
+
/**
|
|
48
|
+
* Recognizes a nonzero integer token using Bash's digit alphabets, without evaluating expressions.
|
|
49
|
+
* Leading zero means octal; base#digits requires an unpadded base from 2..64. Variables and invalid digits
|
|
50
|
+
* provide no static evidence. Machine-integer overflow is outside this lexical recognizer.
|
|
51
|
+
*/
|
|
52
|
+
function isNonzeroInteger(text) {
|
|
53
|
+
if (/^0[xX][0-9a-fA-F]+$/u.test(text))
|
|
54
|
+
return /[1-9a-fA-F]/u.test(text.slice(2));
|
|
55
|
+
if (/^0[0-7]*$/u.test(text))
|
|
56
|
+
return /[1-7]/u.test(text);
|
|
57
|
+
if (/^[1-9][0-9]*$/u.test(text))
|
|
58
|
+
return true;
|
|
59
|
+
const based = /^([1-9]\d?)#([0-9a-zA-Z@_]+)$/u.exec(text);
|
|
60
|
+
if (based === null)
|
|
61
|
+
return false;
|
|
62
|
+
const base = Number(based[1]);
|
|
63
|
+
if (base < 2 || base > 64)
|
|
64
|
+
return false;
|
|
65
|
+
const digits = base <= 36 ? based[2].toLowerCase() : based[2];
|
|
66
|
+
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ@_'.slice(0, base);
|
|
67
|
+
for (const digit of digits) {
|
|
68
|
+
if (!alphabet.includes(digit))
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return /[^0]/u.test(digits);
|
|
72
|
+
}
|
|
45
73
|
/**
|
|
46
74
|
* The `sh/compat/no-negative-array-index` rule module.
|
|
47
75
|
*
|
|
@@ -57,13 +85,14 @@ export const noNegativeArrayIndex = createCompatRule({
|
|
|
57
85
|
description: 'Disallow negative array subscripts (`${arr[-1]}`) when the target shell lacks them',
|
|
58
86
|
},
|
|
59
87
|
messages: {
|
|
60
|
-
negativeIndex: 'Negative array subscripts need {{required}}, but this file
|
|
88
|
+
negativeIndex: 'Negative array subscripts need {{required}}, but this file must support {{target}}. Count from the length instead, for example `${arr[$((${#arr[@]}-1))]}`.',
|
|
61
89
|
useArrayLength: 'Replace with `${{{name}}[$((${#{{name}}[@]}-{{n}}))]}` (the two differ only for an array with unset elements).',
|
|
62
90
|
},
|
|
63
91
|
},
|
|
64
92
|
create(context, gate) {
|
|
65
93
|
const { sourceCode } = context;
|
|
66
|
-
|
|
94
|
+
const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
|
|
95
|
+
/** Whether `index` is a unary minus on a nonzero static integer token. */
|
|
67
96
|
function isNegativeLiteral(index) {
|
|
68
97
|
if (index?.type !== 'UnaryArithm' || index.op !== Minus)
|
|
69
98
|
return false;
|
|
@@ -73,12 +102,12 @@ export const noNegativeArrayIndex = createCompatRule({
|
|
|
73
102
|
if (operand?.type !== 'Word')
|
|
74
103
|
return false;
|
|
75
104
|
const digits = sourceCode.resolveWordText(operand) ?? '';
|
|
76
|
-
return
|
|
105
|
+
return isNonzeroInteger(digits);
|
|
77
106
|
}
|
|
78
107
|
return {
|
|
79
108
|
CallExpr(node) {
|
|
80
109
|
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
81
|
-
transparentWrappers
|
|
110
|
+
transparentWrappers,
|
|
82
111
|
});
|
|
83
112
|
if (invoked?.effective.static !== true || invoked.effective.text !== 'unset')
|
|
84
113
|
return;
|
|
@@ -89,7 +118,8 @@ export const noNegativeArrayIndex = createCompatRule({
|
|
|
89
118
|
const text = sourceCode.resolveWordText(word) ?? sourceCode.getText(word);
|
|
90
119
|
if (text === '--')
|
|
91
120
|
continue;
|
|
92
|
-
|
|
121
|
+
const match = UNSET_NEGATIVE.exec(text);
|
|
122
|
+
if (match !== null && isNonzeroInteger(match[1])) {
|
|
93
123
|
context.report({ node: word, messageId: 'negativeIndex', data: { ...gate } });
|
|
94
124
|
}
|
|
95
125
|
}
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
* - whether the file turns `nounset` on anywhere: a `set` command that
|
|
25
25
|
* enables it (`set -u`, `set -eu`, `set -o nounset`), or a shebang that
|
|
26
26
|
* passes the flag (`#!/bin/bash -eu`). Where in the file that happens is
|
|
27
|
-
* not
|
|
28
|
-
* it;
|
|
27
|
+
* not itself a proof of execution order: a function defined before
|
|
28
|
+
* `set -u` can run after it;
|
|
29
29
|
* - every `${name[@]}` and `${name[*]}` with no operator, except inside
|
|
30
30
|
* `${name[@]+…}` or `${name[@]:+…}` of the same array (the guard this rule
|
|
31
31
|
* suggests), whose word Bash expands only when the array is set. `${#a[@]}`,
|
|
@@ -34,9 +34,17 @@
|
|
|
34
34
|
* replacement `${a[@]/x/y}` were observed to work on Bash 3.2 to 4.1 and
|
|
35
35
|
* fail only on 4.2 and 4.3, which a minimum-version gate cannot express, so
|
|
36
36
|
* they are not reported either.
|
|
37
|
+
*
|
|
38
|
+
* Local safety proofs suppress candidates when a preceding static `set +u`,
|
|
39
|
+
* literal non-empty array assignment, or length guard establishes safety in
|
|
40
|
+
* the same statement list. Calls and compound statements invalidate facts;
|
|
41
|
+
* dynamic environment changes and variable attributes limit these proofs.
|
|
42
|
+
* `nounset-array-proof.ts` owns those boundaries. Reports remain conditional:
|
|
43
|
+
* unknown state is a reason to report, not proof that the array is empty.
|
|
37
44
|
*/
|
|
38
45
|
import { AlternateUnset, AlternateUnsetOrNull } from 'sh-ast';
|
|
39
46
|
import { createCompatRule } from './create-compat-rule.js';
|
|
47
|
+
import { createArraySafetyProof } from './nounset-array-proof.js';
|
|
40
48
|
import { parseSetArguments, shebangOptionChanges } from '../set-options.js';
|
|
41
49
|
/** A shell variable name. */
|
|
42
50
|
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
@@ -68,7 +76,7 @@ export const noNounsetEmptyArray = createCompatRule({
|
|
|
68
76
|
description: 'Disallow whole-array expansions under `set -u` when the target shell aborts on an empty array',
|
|
69
77
|
},
|
|
70
78
|
messages: {
|
|
71
|
-
emptyArray: 'If this array is empty, `${{{name}}[{{subscript}}]}` under `set -u` needs {{required}}, but this file
|
|
79
|
+
emptyArray: 'This expansion is not proven safe. If this array is empty, `${{{name}}[{{subscript}}]}` under `set -u` needs {{required}}, but this file must support {{target}}, where it aborts with "unbound variable". Guard it, for example `{{example}}`.',
|
|
72
80
|
guardExpansion: 'Replace with `{{replacement}}`.',
|
|
73
81
|
},
|
|
74
82
|
},
|
|
@@ -132,9 +140,12 @@ export const noNounsetEmptyArray = createCompatRule({
|
|
|
132
140
|
candidates.push({ node, name, subscript });
|
|
133
141
|
},
|
|
134
142
|
'File:exit'() {
|
|
135
|
-
if (!nounset)
|
|
143
|
+
if (!nounset || candidates.length === 0)
|
|
136
144
|
return;
|
|
145
|
+
const provenSafe = createArraySafetyProof(sourceCode);
|
|
137
146
|
for (const { node, name, subscript } of candidates) {
|
|
147
|
+
if (provenSafe(node, name))
|
|
148
|
+
continue;
|
|
138
149
|
const replacement = guardedExpansion(name, subscript);
|
|
139
150
|
const quoted = wholeQuotedWord(node);
|
|
140
151
|
context.report({
|
|
@@ -56,8 +56,8 @@ export const noParameterTransformation = createMultiFeatureCompatRule({
|
|
|
56
56
|
description: 'Disallow parameter transformations (`${v@Q}`, `${v@U}` and the others) when the target shell lacks the operator',
|
|
57
57
|
},
|
|
58
58
|
messages: {
|
|
59
|
-
transformation: '`${…@{{operator}}}` needs {{required}}, but this file
|
|
60
|
-
transformationNoAlternative: '`${…@{{operator}}}` needs {{required}}, but this file
|
|
59
|
+
transformation: '`${…@{{operator}}}` needs {{required}}, but this file must support {{target}}. Use {{alternative}} instead.',
|
|
60
|
+
transformationNoAlternative: '`${…@{{operator}}}` needs {{required}}, but this file must support {{target}}, which has no equivalent expansion; restructure the code that needs it.',
|
|
61
61
|
},
|
|
62
62
|
},
|
|
63
63
|
create(context, gates) {
|
|
@@ -31,7 +31,7 @@ export const noPipeAll = createCompatRule({
|
|
|
31
31
|
description: 'Disallow the `|&` pipe when the target shell lacks it',
|
|
32
32
|
},
|
|
33
33
|
messages: {
|
|
34
|
-
pipeAll: '`|&` needs {{required}}, but this file
|
|
34
|
+
pipeAll: '`|&` needs {{required}}, but this file must support {{target}}. Write `cmd 2>&1 | other` instead.',
|
|
35
35
|
useStderrRedirect: 'Replace `|&` with `2>&1 |`.',
|
|
36
36
|
},
|
|
37
37
|
},
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* word.
|
|
17
17
|
*/
|
|
18
18
|
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
19
|
+
import { resolveTransparentWrappers } from '../resolve-transparent-wrappers.js';
|
|
19
20
|
import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
|
|
20
21
|
import { createCompatRule } from './create-compat-rule.js';
|
|
21
22
|
/**
|
|
@@ -65,15 +66,16 @@ export const noPrintfTimeFormat = createCompatRule({
|
|
|
65
66
|
description: "Disallow `printf`'s `%(fmt)T` date format when the target shell lacks it",
|
|
66
67
|
},
|
|
67
68
|
messages: {
|
|
68
|
-
printfTime: "`printf '%({{format}})T'` needs {{required}}, but this file
|
|
69
|
+
printfTime: "`printf '%({{format}})T'` needs {{required}}, but this file must support {{target}}. For the current time, run `date '+{{format}}'` instead.",
|
|
69
70
|
},
|
|
70
71
|
},
|
|
71
72
|
create(context, gate) {
|
|
72
73
|
const { sourceCode } = context;
|
|
74
|
+
const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
|
|
73
75
|
return {
|
|
74
76
|
CallExpr(node) {
|
|
75
77
|
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
76
|
-
transparentWrappers
|
|
78
|
+
transparentWrappers,
|
|
77
79
|
});
|
|
78
80
|
if (invoked?.effective.static !== true || invoked.effective.text !== 'printf')
|
|
79
81
|
return;
|
|
@@ -81,9 +81,10 @@ export const noSource = createCompatRule({
|
|
|
81
81
|
create(context, gate) {
|
|
82
82
|
const { sourceCode } = context;
|
|
83
83
|
const fileDefinesSource = definedFunctionNames(sourceCode).has(SOURCE);
|
|
84
|
+
const reachableWord = reachableCommandWord(context, SOURCE);
|
|
84
85
|
return {
|
|
85
86
|
CallExpr(node) {
|
|
86
|
-
const word =
|
|
87
|
+
const word = reachableWord(node);
|
|
87
88
|
if (word === undefined)
|
|
88
89
|
return;
|
|
89
90
|
// Only a name written directly can run a function: a wrapper skips it.
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { TsVarSet } from 'sh-ast';
|
|
29
29
|
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
30
|
+
import { resolveTransparentWrappers } from '../resolve-transparent-wrappers.js';
|
|
30
31
|
import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
|
|
31
32
|
import { createCompatRule } from './create-compat-rule.js';
|
|
32
33
|
/**
|
|
@@ -214,11 +215,12 @@ export const noTestV = createCompatRule({
|
|
|
214
215
|
description: 'Disallow the `-v name` test (`[[ -v x ]]`, `test -v x`) when the target shell lacks it',
|
|
215
216
|
},
|
|
216
217
|
messages: {
|
|
217
|
-
testV: '`-v {{name}}` needs {{required}}, but this file
|
|
218
|
+
testV: '`-v {{name}}` needs {{required}}, but this file must support {{target}}. Test whether the variable is set with `[ -n "${{{name}}+set}" ]` instead.',
|
|
218
219
|
},
|
|
219
220
|
},
|
|
220
221
|
create(context, gate) {
|
|
221
222
|
const { sourceCode } = context;
|
|
223
|
+
const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
|
|
222
224
|
/** The variable name shown in the message for `operand`. */
|
|
223
225
|
function nameOf(operand) {
|
|
224
226
|
const text = operand?.type === 'Word' ? sourceCode.resolveWordText(operand) : null;
|
|
@@ -251,7 +253,7 @@ export const noTestV = createCompatRule({
|
|
|
251
253
|
},
|
|
252
254
|
CallExpr(node) {
|
|
253
255
|
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
254
|
-
transparentWrappers
|
|
256
|
+
transparentWrappers,
|
|
255
257
|
});
|
|
256
258
|
if (invoked?.effective.static !== true)
|
|
257
259
|
return;
|
|
@@ -85,8 +85,8 @@ export const noUnicodeEscape = createMultiFeatureCompatRule({
|
|
|
85
85
|
description: "Disallow `\\u`/`\\U` escapes in `$'…'` strings when the target shell does not decode them to UTF-8",
|
|
86
86
|
},
|
|
87
87
|
messages: {
|
|
88
|
-
unicodeEscape: "`{{escape}}` in `$'…'` needs {{required}}, but this file
|
|
89
|
-
unicodeEscapeLatin1: "`{{escape}}` in `$'…'` needs {{required}}, but this file
|
|
88
|
+
unicodeEscape: "`{{escape}}` in `$'…'` needs {{required}}, but this file must support {{target}}, which leaves it as literal text. Write the UTF-8 bytes as octal escapes instead, for example `$'{{octal}}'`.",
|
|
89
|
+
unicodeEscapeLatin1: "`{{escape}}` in `$'…'` needs {{required}}, but this file must support {{target}}, which writes U+0080 to U+00FF as a single Latin-1 byte, not UTF-8. Write the UTF-8 bytes as octal escapes instead, for example `$'{{octal}}'`.",
|
|
90
90
|
},
|
|
91
91
|
},
|
|
92
92
|
create(context, gates) {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* the option word.
|
|
17
17
|
*/
|
|
18
18
|
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
19
|
+
import { resolveTransparentWrappers } from '../resolve-transparent-wrappers.js';
|
|
19
20
|
import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
|
|
20
21
|
import { createCompatRule } from './create-compat-rule.js';
|
|
21
22
|
/**
|
|
@@ -42,15 +43,16 @@ export const noWaitN = createCompatRule({
|
|
|
42
43
|
description: 'Disallow `wait -n` when the target shell lacks it',
|
|
43
44
|
},
|
|
44
45
|
messages: {
|
|
45
|
-
waitNext: '`wait -n` needs {{required}}, but this file
|
|
46
|
+
waitNext: '`wait -n` needs {{required}}, but this file must support {{target}}. Wait for one job at a time instead: keep each job\'s `$!` and run `wait "$pid"`, which returns that job\'s exit status.',
|
|
46
47
|
},
|
|
47
48
|
},
|
|
48
49
|
create(context, gate) {
|
|
49
50
|
const { sourceCode } = context;
|
|
51
|
+
const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
|
|
50
52
|
return {
|
|
51
53
|
CallExpr(node) {
|
|
52
54
|
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
53
|
-
transparentWrappers
|
|
55
|
+
transparentWrappers,
|
|
54
56
|
});
|
|
55
57
|
if (invoked?.effective.static !== true || invoked.effective.text !== 'wait')
|
|
56
58
|
return;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ShellSourceCode } from '../../language/shell-source-code.js';
|
|
2
|
+
import type { ShellNodeOf } from '../../language/syntax-view.js';
|
|
3
|
+
/**
|
|
4
|
+
* Binds a proof query to one parsed file. The rule retains responsibility for
|
|
5
|
+
* selecting expansions and version gating; this helper only answers safety.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function createArraySafetyProof(source: ShellSourceCode): (node: ShellNodeOf<'ParamExp'>, name: string) => boolean;
|