@eslint-sh/eslint-plugin 0.7.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 +14 -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 +4 -3
- package/dist/configs/posix-portable.d.ts +1 -1
- 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.js +4 -4
- 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 +9 -9
- package/dist/rules/compat/version-gates.js +13 -13
- package/dist/rules/create-wrapper-rule.js +12 -7
- 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/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 +76 -27
- package/docs/custom-rule-tutorial.md +9 -3
- package/docs/examples/no-listed-commands-rule.ts +2 -2
- package/docs/rules/README.md +36 -4
- 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 +12 -12
- 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/shellcheck.md +9 -5
- package/docs/transparent-wrappers.md +97 -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
|
},
|
|
@@ -236,10 +236,10 @@ export const noBraceSequencePaddingStep = createCompatRule({
|
|
|
236
236
|
description: 'Disallow zero-padded (`{01..10}`) and stepped (`{1..10..2}`) brace sequences when the target shell lacks them',
|
|
237
237
|
},
|
|
238
238
|
messages: {
|
|
239
|
-
padded: '`{{sequence}}` needs {{required}}, but this file
|
|
240
|
-
stepped: '`{{sequence}}` needs {{required}}, but this file
|
|
241
|
-
posixPadded: '`{{sequence}}` needs {{required}}, but this file
|
|
242
|
-
posixStepped: '`{{sequence}}` needs {{required}}, but this file
|
|
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
243
|
},
|
|
244
244
|
},
|
|
245
245
|
create(context, gate) {
|
|
@@ -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
|
},
|
|
@@ -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;
|