@eslint-sh/eslint-plugin 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -17
- package/dist/command-harvest/builtins/bash.d.ts +36 -0
- package/dist/command-harvest/builtins/bash.js +98 -0
- package/dist/command-harvest/builtins/bats.d.ts +45 -0
- package/dist/command-harvest/builtins/bats.js +33 -0
- package/dist/command-harvest/builtins/index.d.ts +18 -0
- package/dist/command-harvest/builtins/index.js +39 -0
- package/dist/command-harvest/builtins/posix.d.ts +51 -0
- package/dist/command-harvest/builtins/posix.js +93 -0
- package/dist/command-harvest/harvest-commands.d.ts +66 -0
- package/dist/command-harvest/harvest-commands.js +97 -0
- package/dist/command-harvest/wrapper-table.d.ts +24 -0
- package/dist/command-harvest/wrapper-table.js +49 -0
- package/dist/configs/agent-authored.d.ts +41 -0
- package/dist/configs/agent-authored.js +33 -0
- package/dist/configs/bash-3-2.d.ts +28 -0
- package/dist/configs/bash-3-2.js +51 -0
- package/dist/configs/posix-portable.d.ts +40 -0
- package/dist/configs/posix-portable.js +53 -0
- package/dist/configs/recommended.d.ts +52 -0
- package/dist/configs/recommended.js +34 -0
- package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
- package/dist/declaration-conventions/comment-pragma.js +153 -0
- package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
- package/dist/declaration-conventions/declaration-convention.js +1 -0
- package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
- package/dist/declaration-conventions/read-declared-commands.js +128 -0
- package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
- package/dist/eslint-plugin-syntax.d.ts +27 -0
- package/dist/eslint-plugin.d.ts +1211 -52
- package/dist/index.d.ts +56 -8
- package/dist/index.js +53 -6
- package/dist/language/command-resolution.d.ts +124 -0
- package/dist/language/command-resolution.js +120 -0
- package/dist/language/end-of-input-marker.d.ts +29 -0
- package/dist/language/end-of-input-marker.js +88 -0
- package/dist/language/line-breaks.d.ts +18 -0
- package/dist/language/line-breaks.js +22 -0
- package/dist/language/lint-view-types.d.ts +104 -0
- package/dist/language/lint-view-types.js +1 -0
- package/dist/language/non-shell-file-guard.d.ts +35 -0
- package/dist/language/non-shell-file-guard.js +59 -0
- package/dist/language/selector-classes.d.ts +0 -1
- package/dist/language/selector-classes.js +0 -1
- package/dist/language/shell-language-options.d.ts +7 -1
- package/dist/language/shell-language-options.js +4 -4
- package/dist/language/shell-language.d.ts +10 -2
- package/dist/language/shell-language.js +59 -8
- package/dist/language/shell-source-code.d.ts +342 -5
- package/dist/language/shell-source-code.js +559 -30
- package/dist/language/source-coordinates.d.ts +13 -2
- package/dist/language/source-coordinates.js +68 -14
- package/dist/language/syntax-view.d.ts +60 -39
- package/dist/language/syntax-view.js +200 -45
- package/dist/language/word-resolution.d.ts +4 -3
- package/dist/language/word-resolution.js +4 -3
- package/dist/rules/compat/create-compat-rule.d.ts +59 -0
- package/dist/rules/compat/create-compat-rule.js +65 -0
- package/dist/rules/compat/declaration-options.d.ts +19 -0
- package/dist/rules/compat/declaration-options.js +110 -0
- package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
- package/dist/rules/compat/no-append-all-redirect.js +73 -0
- package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
- package/dist/rules/compat/no-associative-arrays.js +39 -0
- package/dist/rules/compat/no-brace-sequence-padding-step.d.ts +15 -0
- package/dist/rules/compat/no-brace-sequence-padding-step.js +287 -0
- package/dist/rules/compat/no-case-attributes.d.ts +12 -0
- package/dist/rules/compat/no-case-attributes.js +63 -0
- package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
- package/dist/rules/compat/no-case-fallthrough.js +67 -0
- package/dist/rules/compat/no-case-modification.d.ts +35 -0
- package/dist/rules/compat/no-case-modification.js +86 -0
- package/dist/rules/compat/no-coproc.d.ts +12 -0
- package/dist/rules/compat/no-coproc.js +47 -0
- package/dist/rules/compat/no-declare-g.d.ts +12 -0
- package/dist/rules/compat/no-declare-g.js +59 -0
- package/dist/rules/compat/no-double-bracket.d.ts +12 -0
- package/dist/rules/compat/no-double-bracket.js +52 -0
- package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
- package/dist/rules/compat/no-epoch-variables.js +57 -0
- package/dist/rules/compat/no-fd-variable.d.ts +12 -0
- package/dist/rules/compat/no-fd-variable.js +48 -0
- package/dist/rules/compat/no-globstar.d.ts +12 -0
- package/dist/rules/compat/no-globstar.js +39 -0
- package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
- package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
- package/dist/rules/compat/no-let.d.ts +12 -0
- package/dist/rules/compat/no-let.js +62 -0
- package/dist/rules/compat/no-mapfile.d.ts +13 -0
- package/dist/rules/compat/no-mapfile.js +122 -0
- package/dist/rules/compat/no-nameref.d.ts +12 -0
- package/dist/rules/compat/no-nameref.js +44 -0
- package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
- package/dist/rules/compat/no-negative-array-index.js +123 -0
- package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
- package/dist/rules/compat/no-nounset-empty-array.js +158 -0
- package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
- package/dist/rules/compat/no-parameter-transformation.js +123 -0
- package/dist/rules/compat/no-pipe-all.d.ts +13 -0
- package/dist/rules/compat/no-pipe-all.js +68 -0
- package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
- package/dist/rules/compat/no-printf-time-format.js +99 -0
- package/dist/rules/compat/no-source.d.ts +13 -0
- package/dist/rules/compat/no-source.js +103 -0
- package/dist/rules/compat/no-test-v.d.ts +12 -0
- package/dist/rules/compat/no-test-v.js +280 -0
- package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
- package/dist/rules/compat/no-unicode-escape.js +120 -0
- package/dist/rules/compat/no-wait-n.d.ts +12 -0
- package/dist/rules/compat/no-wait-n.js +73 -0
- package/dist/rules/compat/operator-range.d.ts +10 -0
- package/dist/rules/compat/operator-range.js +15 -0
- package/dist/rules/compat/posix-absent-command.d.ts +12 -0
- package/dist/rules/compat/posix-absent-command.js +134 -0
- package/dist/rules/compat/shopt-options.d.ts +9 -0
- package/dist/rules/compat/shopt-options.js +63 -0
- package/dist/rules/compat/version-gates.d.ts +76 -0
- package/dist/rules/compat/version-gates.js +205 -0
- package/dist/rules/create-rule.d.ts +91 -0
- package/dist/rules/create-rule.js +51 -0
- package/dist/rules/create-wrapper-rule.d.ts +166 -0
- package/dist/rules/create-wrapper-rule.js +407 -0
- package/dist/rules/define-shell-rule.d.ts +184 -0
- package/dist/rules/define-shell-rule.js +56 -0
- package/dist/rules/index.d.ts +26 -0
- package/dist/rules/index.js +90 -0
- package/dist/rules/max-script-complexity.d.ts +28 -0
- package/dist/rules/max-script-complexity.js +130 -0
- package/dist/rules/no-dynamic-invocation.d.ts +35 -0
- package/dist/rules/no-dynamic-invocation.js +117 -0
- package/dist/rules/no-eval.d.ts +13 -0
- package/dist/rules/no-eval.js +89 -0
- package/dist/rules/no-masked-exit-status.d.ts +21 -0
- package/dist/rules/no-masked-exit-status.js +667 -0
- package/dist/rules/no-secret-env-reads.d.ts +24 -0
- package/dist/rules/no-secret-env-reads.js +133 -0
- package/dist/rules/no-secret-flags.d.ts +32 -0
- package/dist/rules/no-secret-flags.js +162 -0
- package/dist/rules/no-unchecked-cd.d.ts +44 -0
- package/dist/rules/no-unchecked-cd.js +250 -0
- package/dist/rules/no-undeclared-commands.d.ts +20 -0
- package/dist/rules/no-undeclared-commands.js +115 -0
- package/dist/rules/no-unused-declarations.d.ts +23 -0
- package/dist/rules/no-unused-declarations.js +259 -0
- package/dist/rules/prefer-local.d.ts +16 -0
- package/dist/rules/prefer-local.js +249 -0
- package/dist/rules/quote-expansions.d.ts +13 -0
- package/dist/rules/quote-expansions.js +229 -0
- package/dist/rules/require-network-wrapper.d.ts +40 -0
- package/dist/rules/require-network-wrapper.js +244 -0
- package/dist/rules/require-shebang.d.ts +29 -0
- package/dist/rules/require-shebang.js +101 -0
- package/dist/rules/require-strict-mode.d.ts +52 -0
- package/dist/rules/require-strict-mode.js +177 -0
- package/dist/rules/require-tty-guard.d.ts +13 -0
- package/dist/rules/require-tty-guard.js +318 -0
- package/dist/rules/resolve-declaration-convention.d.ts +96 -0
- package/dist/rules/resolve-declaration-convention.js +109 -0
- package/dist/rules/resolve-network-wrapper.d.ts +19 -0
- package/dist/rules/resolve-network-wrapper.js +116 -0
- package/dist/rules/set-options.d.ts +109 -0
- package/dist/rules/set-options.js +201 -0
- package/dist/rules/shebang-interpreter.d.ts +18 -0
- package/dist/rules/shebang-interpreter.js +81 -0
- package/dist/rules/shebang-matches-dialect.d.ts +12 -0
- package/dist/rules/shebang-matches-dialect.js +69 -0
- package/dist/rules/transparent-wrappers.d.ts +52 -0
- package/dist/rules/transparent-wrappers.js +99 -0
- package/dist/rules/user-pattern.d.ts +63 -0
- package/dist/rules/user-pattern.js +184 -0
- package/dist/syntax/analyze.d.ts +19 -0
- package/dist/syntax/analyze.js +21 -0
- package/dist/syntax/index.d.ts +23 -0
- package/dist/syntax/index.js +25 -0
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -1
- package/docs/compat.md +354 -0
- package/docs/custom-rule-tutorial.md +780 -34
- package/docs/declaration-conventions.md +478 -0
- package/docs/examples/audit-wrapper-pack.mjs +15 -0
- package/docs/examples/dep-line-convention.mjs +22 -0
- package/docs/examples/eslint.config.mjs +3 -2
- package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
- package/docs/examples/no-backticks-rule-tester.mjs +106 -0
- package/docs/examples/no-backticks-rule.mjs +60 -0
- package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
- package/docs/examples/no-function-keyword-rule.mjs +59 -0
- package/docs/examples/no-listed-commands-rule.ts +63 -0
- package/docs/examples/no-rm-rf-rule.mjs +26 -15
- package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
- package/docs/examples/sidecar-convention.mjs +38 -0
- package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
- package/docs/examples/sorted-declarations-rule.mjs +52 -0
- package/docs/reading-nodes.md +159 -0
- package/docs/rules/README.md +79 -0
- package/docs/rules/_template.md +83 -0
- package/docs/rules/compat/no-append-all-redirect.md +87 -0
- package/docs/rules/compat/no-associative-arrays.md +95 -0
- package/docs/rules/compat/no-brace-sequence-padding-step.md +90 -0
- package/docs/rules/compat/no-case-attributes.md +82 -0
- package/docs/rules/compat/no-case-fallthrough.md +90 -0
- package/docs/rules/compat/no-case-modification.md +74 -0
- package/docs/rules/compat/no-coproc.md +76 -0
- package/docs/rules/compat/no-declare-g.md +80 -0
- package/docs/rules/compat/no-double-bracket.md +72 -0
- package/docs/rules/compat/no-epoch-variables.md +79 -0
- package/docs/rules/compat/no-fd-variable.md +78 -0
- package/docs/rules/compat/no-globstar.md +74 -0
- package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
- package/docs/rules/compat/no-let.md +78 -0
- package/docs/rules/compat/no-mapfile.md +118 -0
- package/docs/rules/compat/no-nameref.md +81 -0
- package/docs/rules/compat/no-negative-array-index.md +87 -0
- package/docs/rules/compat/no-nounset-empty-array.md +103 -0
- package/docs/rules/compat/no-parameter-transformation.md +94 -0
- package/docs/rules/compat/no-pipe-all.md +85 -0
- package/docs/rules/compat/no-printf-time-format.md +75 -0
- package/docs/rules/compat/no-source.md +98 -0
- package/docs/rules/compat/no-test-v.md +82 -0
- package/docs/rules/compat/no-unicode-escape.md +91 -0
- package/docs/rules/compat/no-wait-n.md +85 -0
- package/docs/rules/max-script-complexity.md +109 -0
- package/docs/rules/no-dynamic-invocation.md +139 -0
- package/docs/rules/no-eval.md +106 -0
- package/docs/rules/no-masked-exit-status.md +239 -0
- package/docs/rules/no-secret-env-reads.md +173 -0
- package/docs/rules/no-secret-flags.md +169 -0
- package/docs/rules/no-unchecked-cd.md +146 -0
- package/docs/rules/no-undeclared-commands.md +141 -0
- package/docs/rules/no-unused-declarations.md +216 -0
- package/docs/rules/prefer-local.md +221 -0
- package/docs/rules/quote-expansions.md +157 -0
- package/docs/rules/require-network-wrapper.md +171 -0
- package/docs/rules/require-shebang.md +157 -0
- package/docs/rules/require-strict-mode.md +253 -0
- package/docs/rules/require-tty-guard.md +131 -0
- package/docs/rules/shebang-matches-dialect.md +120 -0
- package/docs/selectors.md +2 -2
- package/docs/shellcheck.md +87 -0
- package/docs/untrusted-code.md +440 -0
- package/docs/wrapper-rules.md +206 -0
- package/package.json +26 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/language/selector-classes.d.ts.map +0 -1
- package/dist/language/selector-classes.js.map +0 -1
- package/dist/language/shell-language-options.d.ts.map +0 -1
- package/dist/language/shell-language-options.js.map +0 -1
- package/dist/language/shell-language.d.ts.map +0 -1
- package/dist/language/shell-language.js.map +0 -1
- package/dist/language/shell-source-code.d.ts.map +0 -1
- package/dist/language/shell-source-code.js.map +0 -1
- package/dist/language/source-coordinates.d.ts.map +0 -1
- package/dist/language/source-coordinates.js.map +0 -1
- package/dist/language/syntax-view.d.ts.map +0 -1
- package/dist/language/syntax-view.js.map +0 -1
- package/dist/language/word-resolution.d.ts.map +0 -1
- package/dist/language/word-resolution.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-source`: reports the `source` builtin in files that target
|
|
3
|
+
* POSIX sh, where the portable name is the `.` special built-in. Every Bash has
|
|
4
|
+
* `source`, so this is a POSIX-only gate.
|
|
5
|
+
*
|
|
6
|
+
* POSIX section 2.9.1.4 leaves the results of a command named `source`
|
|
7
|
+
* unspecified. dash has no such command: `source ./lib.sh` fails with `source:
|
|
8
|
+
* not found` and status 127, and the file is never read. `bash --posix` and
|
|
9
|
+
* BusyBox ash accept it, so only some POSIX shells show the problem.
|
|
10
|
+
*
|
|
11
|
+
* What is read: a command whose effective name is `source`, outside a branch
|
|
12
|
+
* for one shell (see `./posix-absent-command.ts`).
|
|
13
|
+
*
|
|
14
|
+
* A function named `source` anywhere in the file exempts a call that names it
|
|
15
|
+
* directly, since dash can then run that function; `bash --posix` refuses that
|
|
16
|
+
* definition, which this rule does not judge. This is a name check across the
|
|
17
|
+
* file and does not prove the function exists at the call: execution order, a
|
|
18
|
+
* definition in a branch or subshell, and `unset -f` are not read, so some exempt
|
|
19
|
+
* calls still fail on dash. A call through a wrapper is never exempt, because
|
|
20
|
+
* wrappers such as `command`, `env` and `exec` do not run functions.
|
|
21
|
+
*
|
|
22
|
+
* The suggestion replaces the word with `.` only where the two read the same
|
|
23
|
+
* file with the same effect, apart from a file that cannot be read: `.` then
|
|
24
|
+
* ends the script on a POSIX shell, so a `|| fallback` never runs, and the
|
|
25
|
+
* suggestion's text says so. The conditions are:
|
|
26
|
+
*
|
|
27
|
+
* - The command is written directly, with no wrapper.
|
|
28
|
+
* - It has no leading `VAR=value`. Such an assignment persists after `.` on a
|
|
29
|
+
* POSIX shell, while Bash outside POSIX mode scopes it to the `source`
|
|
30
|
+
* command.
|
|
31
|
+
* - It has exactly one operand. `.` takes one in POSIX, and dash ignores any
|
|
32
|
+
* others where `source` would set the positional parameters.
|
|
33
|
+
* - The operand has a literal `/`. A name without one makes `.` search `PATH`.
|
|
34
|
+
*/
|
|
35
|
+
import { definedFunctionNames } from '../../command-harvest/harvest-commands.js';
|
|
36
|
+
import { childNodes } from '../../language/syntax-view.js';
|
|
37
|
+
import { createCompatRule } from './create-compat-rule.js';
|
|
38
|
+
import { reachableCommandWord } from './posix-absent-command.js';
|
|
39
|
+
/** The builtin this rule gates. */
|
|
40
|
+
const SOURCE = 'source';
|
|
41
|
+
/**
|
|
42
|
+
* Whether `word` holds a `/` that no expansion can remove: in unquoted or
|
|
43
|
+
* quoted literal text, whatever its other parts expand to. A `/` inside an
|
|
44
|
+
* expansion (`${x%/*}`, `$(pwd)`) does not count, so the walk stops at anything
|
|
45
|
+
* that is not literal text or a double-quoted group.
|
|
46
|
+
*/
|
|
47
|
+
function hasLiteralSlash(word) {
|
|
48
|
+
return childNodes(word).some((part) => {
|
|
49
|
+
if (part.type === 'Lit' || part.type === 'SglQuoted')
|
|
50
|
+
return part.value.includes('/');
|
|
51
|
+
return part.type === 'DblQuoted' && hasLiteralSlash(part);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Whether `.` is an exact replacement for this `source` command (see the module comment). */
|
|
55
|
+
function dotIsEquivalent(sourceCode, node) {
|
|
56
|
+
if (sourceCode.getCommandName(node) !== SOURCE)
|
|
57
|
+
return false;
|
|
58
|
+
if (node.assigns.length > 0 || node.args.length !== 2)
|
|
59
|
+
return false;
|
|
60
|
+
return hasLiteralSlash(node.args[1]);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The `sh/compat/no-source` rule module.
|
|
64
|
+
*
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
export const noSource = createCompatRule({
|
|
68
|
+
name: 'compat/no-source',
|
|
69
|
+
feature: 'sourceCommand',
|
|
70
|
+
meta: {
|
|
71
|
+
type: 'problem',
|
|
72
|
+
hasSuggestions: true,
|
|
73
|
+
docs: {
|
|
74
|
+
description: 'Disallow the `source` builtin when the target shell is POSIX sh',
|
|
75
|
+
},
|
|
76
|
+
messages: {
|
|
77
|
+
source: '`source` is not part of {{target}}. dash fails with `source: not found` and the file is never read. Use the `.` special built-in with a path that contains a slash, for example `. ./lib.sh`.',
|
|
78
|
+
useDot: 'Replace `source` with `.`, which ends the script if the file is missing.',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
create(context, gate) {
|
|
82
|
+
const { sourceCode } = context;
|
|
83
|
+
const fileDefinesSource = definedFunctionNames(sourceCode).has(SOURCE);
|
|
84
|
+
return {
|
|
85
|
+
CallExpr(node) {
|
|
86
|
+
const word = reachableCommandWord(sourceCode, node, SOURCE);
|
|
87
|
+
if (word === undefined)
|
|
88
|
+
return;
|
|
89
|
+
// Only a name written directly can run a function: a wrapper skips it.
|
|
90
|
+
if (fileDefinesSource && sourceCode.getCommandName(node) === SOURCE)
|
|
91
|
+
return;
|
|
92
|
+
context.report({
|
|
93
|
+
node: word,
|
|
94
|
+
messageId: 'source',
|
|
95
|
+
data: { target: gate.target },
|
|
96
|
+
suggest: dotIsEquivalent(sourceCode, node)
|
|
97
|
+
? [{ messageId: 'useDot', fix: (fixer) => fixer.replaceText(word, '.') }]
|
|
98
|
+
: [],
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export type NoTestVMessageId = 'testV';
|
|
7
|
+
/**
|
|
8
|
+
* The `sh/compat/no-test-v` rule module.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const noTestV: import("../define-shell-rule.js").ShellRule<[], "testV">;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-test-v`: reports the `-v name` test (`[[ -v x ]]`,
|
|
3
|
+
* `test -v x`, `[ -v x ]`) in files whose target shell lacks it, that is,
|
|
4
|
+
* Bash older than 4.2 (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
|
|
5
|
+
*
|
|
6
|
+
* In Bash 4.1 and older, `[[ -v x ]]` is a syntax error, so the whole script
|
|
7
|
+
* fails to parse, and `test -v x` fails with "unary operator expected". The
|
|
8
|
+
* message suggests the portable alternative, `[ -n "${x+set}" ]`, which is
|
|
9
|
+
* true exactly when `x` is set (even to the empty string) and is safe under
|
|
10
|
+
* `set -u`.
|
|
11
|
+
*
|
|
12
|
+
* What is read:
|
|
13
|
+
*
|
|
14
|
+
* - inside `[[ … ]]`, the parser's `-v` unary test node;
|
|
15
|
+
* - for `test` and `[` (after transparent wrappers such as `builtin`), the
|
|
16
|
+
* arguments, read the way Bash's `test` reads them: with two arguments a
|
|
17
|
+
* leading `-v` is the operator; with three, the middle one is a binary
|
|
18
|
+
* operator if it can be one, and `! -v name` negates the test; with four,
|
|
19
|
+
* `! …` and `( … )` peel off; anything longer goes through `test`'s own
|
|
20
|
+
* precedence parser, where `-v` is the operator only where a primary is
|
|
21
|
+
* expected and another argument follows it. So `[ "$x" = -v ]` and
|
|
22
|
+
* `[ -n -v ]` do not report. A double-quoted expansion (`"$name"`) is one
|
|
23
|
+
* operand whatever its value. Any other argument that is not a static word
|
|
24
|
+
* ends the reading, because it may expand to several arguments or none.
|
|
25
|
+
*
|
|
26
|
+
* The report is on the `[[` test, or on the `-v` word.
|
|
27
|
+
*/
|
|
28
|
+
import { TsVarSet } from 'sh-ast';
|
|
29
|
+
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
30
|
+
import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
|
|
31
|
+
import { createCompatRule } from './create-compat-rule.js';
|
|
32
|
+
/**
|
|
33
|
+
* The wrappers followed to find the `test` command: `sh-ast/analyze`'s
|
|
34
|
+
* defaults (which include `command`) plus Bash's `builtin`.
|
|
35
|
+
*/
|
|
36
|
+
const TRANSPARENT_WRAPPERS = [
|
|
37
|
+
...DEFAULT_TRANSPARENT_WRAPPERS,
|
|
38
|
+
BUILTIN_WRAPPER,
|
|
39
|
+
];
|
|
40
|
+
/** The commands that are Bash's `test` builtin. */
|
|
41
|
+
const TEST_COMMANDS = new Set(['test', '[']);
|
|
42
|
+
/** Bash's `test` binary operators (`test_binop`), without `-a` and `-o`. */
|
|
43
|
+
const BINARY_OPERATORS = new Set([
|
|
44
|
+
'=',
|
|
45
|
+
'==',
|
|
46
|
+
'!=',
|
|
47
|
+
'<',
|
|
48
|
+
'>',
|
|
49
|
+
'-eq',
|
|
50
|
+
'-ne',
|
|
51
|
+
'-lt',
|
|
52
|
+
'-le',
|
|
53
|
+
'-gt',
|
|
54
|
+
'-ge',
|
|
55
|
+
'-nt',
|
|
56
|
+
'-ot',
|
|
57
|
+
'-ef',
|
|
58
|
+
]);
|
|
59
|
+
/** Bash's `test` unary operators (`test_unop`). */
|
|
60
|
+
const UNARY_OPERATORS = new Set([
|
|
61
|
+
'-a',
|
|
62
|
+
'-b',
|
|
63
|
+
'-c',
|
|
64
|
+
'-d',
|
|
65
|
+
'-e',
|
|
66
|
+
'-f',
|
|
67
|
+
'-g',
|
|
68
|
+
'-h',
|
|
69
|
+
'-k',
|
|
70
|
+
'-n',
|
|
71
|
+
'-o',
|
|
72
|
+
'-p',
|
|
73
|
+
'-r',
|
|
74
|
+
'-s',
|
|
75
|
+
'-t',
|
|
76
|
+
'-u',
|
|
77
|
+
'-v',
|
|
78
|
+
'-w',
|
|
79
|
+
'-x',
|
|
80
|
+
'-z',
|
|
81
|
+
'-G',
|
|
82
|
+
'-L',
|
|
83
|
+
'-O',
|
|
84
|
+
'-S',
|
|
85
|
+
'-N',
|
|
86
|
+
'-R',
|
|
87
|
+
]);
|
|
88
|
+
/**
|
|
89
|
+
* What a double-quoted, non-static argument stands for while reading `test`'s
|
|
90
|
+
* arguments: an operand that is never an operator. A NUL cannot occur in a
|
|
91
|
+
* shell word, so no real argument equals it.
|
|
92
|
+
*/
|
|
93
|
+
const QUOTED_OPERAND = '\0operand';
|
|
94
|
+
/** A shell variable name, optionally with an array subscript. */
|
|
95
|
+
const VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(?:\[[^\]]*\])?$/u;
|
|
96
|
+
/**
|
|
97
|
+
* The indices of the arguments in `args` that Bash's `test` reads as the
|
|
98
|
+
* `-v` operator. `args` are the static texts after the command name (and
|
|
99
|
+
* without a closing `]`); reading stops at the first `null`.
|
|
100
|
+
*/
|
|
101
|
+
function variableTestIndices(args) {
|
|
102
|
+
const end = args.indexOf(null);
|
|
103
|
+
const argv = (end === -1 ? args : args.slice(0, end));
|
|
104
|
+
const found = [];
|
|
105
|
+
/** `test`'s two-argument rule, at offset `at`. */
|
|
106
|
+
function twoArguments(at) {
|
|
107
|
+
if (argv[at] === '-v')
|
|
108
|
+
found.push(at);
|
|
109
|
+
}
|
|
110
|
+
/** `test`'s three-argument rule, at offset `at`. */
|
|
111
|
+
function threeArguments(at) {
|
|
112
|
+
const middle = argv[at + 1];
|
|
113
|
+
if (BINARY_OPERATORS.has(middle) || middle === '-a' || middle === '-o')
|
|
114
|
+
return true;
|
|
115
|
+
if (argv[at] === '!') {
|
|
116
|
+
twoArguments(at + 1);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return argv[at] === '(' && argv[at + 2] === ')';
|
|
120
|
+
}
|
|
121
|
+
/** `test`'s precedence parser over every argument (`expr` in Bash's test.c). */
|
|
122
|
+
function expression() {
|
|
123
|
+
let pos = 0;
|
|
124
|
+
const term = () => {
|
|
125
|
+
if (pos >= argv.length)
|
|
126
|
+
return;
|
|
127
|
+
const word = argv[pos];
|
|
128
|
+
if (word === '!') {
|
|
129
|
+
pos++;
|
|
130
|
+
term();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (word === '(') {
|
|
134
|
+
pos++;
|
|
135
|
+
or();
|
|
136
|
+
if (argv[pos] === ')')
|
|
137
|
+
pos++;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (pos + 3 <= argv.length && BINARY_OPERATORS.has(argv[pos + 1])) {
|
|
141
|
+
pos += 3;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (UNARY_OPERATORS.has(word) && pos + 1 < argv.length) {
|
|
145
|
+
if (word === '-v')
|
|
146
|
+
found.push(pos);
|
|
147
|
+
pos += 2;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
pos++;
|
|
151
|
+
};
|
|
152
|
+
const and = () => {
|
|
153
|
+
term();
|
|
154
|
+
while (argv[pos] === '-a') {
|
|
155
|
+
pos++;
|
|
156
|
+
term();
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const or = () => {
|
|
160
|
+
and();
|
|
161
|
+
while (argv[pos] === '-o') {
|
|
162
|
+
pos++;
|
|
163
|
+
and();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
while (pos < argv.length) {
|
|
167
|
+
const before = pos;
|
|
168
|
+
or();
|
|
169
|
+
if (pos === before)
|
|
170
|
+
pos++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
switch (argv.length) {
|
|
174
|
+
case 0:
|
|
175
|
+
case 1:
|
|
176
|
+
break;
|
|
177
|
+
case 2:
|
|
178
|
+
if (argv[0] !== '!')
|
|
179
|
+
twoArguments(0);
|
|
180
|
+
break;
|
|
181
|
+
case 3:
|
|
182
|
+
if (!threeArguments(0))
|
|
183
|
+
expression();
|
|
184
|
+
break;
|
|
185
|
+
case 4:
|
|
186
|
+
if (argv[0] === '!') {
|
|
187
|
+
if (!threeArguments(1))
|
|
188
|
+
expression();
|
|
189
|
+
}
|
|
190
|
+
else if (argv[0] === '(' && argv[3] === ')') {
|
|
191
|
+
if (argv[1] !== '!')
|
|
192
|
+
twoArguments(1);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
expression();
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
default:
|
|
199
|
+
expression();
|
|
200
|
+
}
|
|
201
|
+
return found;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The `sh/compat/no-test-v` rule module.
|
|
205
|
+
*
|
|
206
|
+
* @internal
|
|
207
|
+
*/
|
|
208
|
+
export const noTestV = createCompatRule({
|
|
209
|
+
name: 'compat/no-test-v',
|
|
210
|
+
feature: 'testVariableSet',
|
|
211
|
+
meta: {
|
|
212
|
+
type: 'problem',
|
|
213
|
+
docs: {
|
|
214
|
+
description: 'Disallow the `-v name` test (`[[ -v x ]]`, `test -v x`) when the target shell lacks it',
|
|
215
|
+
},
|
|
216
|
+
messages: {
|
|
217
|
+
testV: '`-v {{name}}` needs {{required}}, but this file targets {{target}}. Test whether the variable is set with `[ -n "${{{name}}+set}" ]` instead.',
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
create(context, gate) {
|
|
221
|
+
const { sourceCode } = context;
|
|
222
|
+
/** The variable name shown in the message for `operand`. */
|
|
223
|
+
function nameOf(operand) {
|
|
224
|
+
const text = operand?.type === 'Word' ? sourceCode.resolveWordText(operand) : null;
|
|
225
|
+
return text !== null && VARIABLE_NAME.test(text) ? text : 'name';
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The argument `word` gives `test`: its static text; a stand-in operand
|
|
229
|
+
* for a word that is one double-quoted string, which always makes exactly
|
|
230
|
+
* one argument whatever it expands to; or `null` for any other word, which
|
|
231
|
+
* may split into several arguments or none.
|
|
232
|
+
*/
|
|
233
|
+
function argumentText(word) {
|
|
234
|
+
const text = sourceCode.resolveWordText(word);
|
|
235
|
+
if (text !== null)
|
|
236
|
+
return text;
|
|
237
|
+
if (word.parts.length === 1 && word.parts[0].type === 'DblQuoted') {
|
|
238
|
+
return QUOTED_OPERAND;
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
UnaryTest(node) {
|
|
244
|
+
if (node.op !== TsVarSet)
|
|
245
|
+
return;
|
|
246
|
+
context.report({
|
|
247
|
+
node,
|
|
248
|
+
messageId: 'testV',
|
|
249
|
+
data: { ...gate, name: nameOf(node.x) },
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
CallExpr(node) {
|
|
253
|
+
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
254
|
+
transparentWrappers: TRANSPARENT_WRAPPERS,
|
|
255
|
+
});
|
|
256
|
+
if (invoked?.effective.static !== true)
|
|
257
|
+
return;
|
|
258
|
+
const command = invoked.effective.text;
|
|
259
|
+
if (!TEST_COMMANDS.has(command))
|
|
260
|
+
return;
|
|
261
|
+
// `chainIndices` index `node.args` directly, so slice before filtering.
|
|
262
|
+
let words = node.args.slice((invoked.chainIndices.at(-1) ?? 0) + 1);
|
|
263
|
+
if (command === '[') {
|
|
264
|
+
const last = words.at(-1);
|
|
265
|
+
if (last === undefined || sourceCode.resolveWordText(last) !== ']')
|
|
266
|
+
return;
|
|
267
|
+
words = words.slice(0, -1);
|
|
268
|
+
}
|
|
269
|
+
const texts = words.map((word) => argumentText(word));
|
|
270
|
+
for (const index of variableTestIndices(texts)) {
|
|
271
|
+
context.report({
|
|
272
|
+
node: words[index],
|
|
273
|
+
messageId: 'testV',
|
|
274
|
+
data: { ...gate, name: nameOf(words[index + 1]) },
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with: `unicodeEscape` when the target leaves
|
|
3
|
+
* the escape undecoded, `unicodeEscapeLatin1` when it decodes it to a
|
|
4
|
+
* Latin-1 byte.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export type NoUnicodeEscapeMessageId = 'unicodeEscape' | 'unicodeEscapeLatin1';
|
|
9
|
+
/**
|
|
10
|
+
* The `sh/compat/no-unicode-escape` rule module.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export declare const noUnicodeEscape: import("../define-shell-rule.js").ShellRule<[], NoUnicodeEscapeMessageId>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-unicode-escape`: reports the `\uXXXX` and `\UXXXXXXXX`
|
|
3
|
+
* escapes in a `$'…'` string in files whose target shell does not decode
|
|
4
|
+
* them to UTF-8. The minimum Bash depends on the code point:
|
|
5
|
+
*
|
|
6
|
+
* - Bash 4.1 and older leave every such escape as literal text, with no
|
|
7
|
+
* error: Bash 4.2 is the first release that decodes them;
|
|
8
|
+
* - the Bash 4.2 release writes a code point from U+0080 to U+00FF as a
|
|
9
|
+
* single Latin-1 byte even in a UTF-8 locale (observed: `e9` for U+00E9,
|
|
10
|
+
* where UTF-8 is `c3 a9`), while every other code point is UTF-8. Bash 4.3
|
|
11
|
+
* is the first release that writes them all as UTF-8.
|
|
12
|
+
*
|
|
13
|
+
* The message suggests the portable alternative: write the character's
|
|
14
|
+
* UTF-8 bytes as octal escapes, which every Bash decodes in `$'…'` (for
|
|
15
|
+
* example `$'\303\251'` for U+00E9). Under `dialect: "posix"` the parser
|
|
16
|
+
* reads `$'…'` as a literal `$` followed by an ordinary single-quoted string,
|
|
17
|
+
* as dash does, so there is no `$'…'` string to report; the gate's
|
|
18
|
+
* `posix: false` records that POSIX sh does not decode these escapes.
|
|
19
|
+
*
|
|
20
|
+
* What is read: a `$'…'` string's text, scanned as Bash decodes it: `\\` is a
|
|
21
|
+
* backslash, `\cX` is one control character (so in `\cu00e9` the `u`
|
|
22
|
+
* belongs to `\c`), and `\u` takes one to four hex digits, `\U` one to
|
|
23
|
+
* eight. A `\u` with no hex digit after it is literal text. Plain `'…'`
|
|
24
|
+
* and `"…"` strings do not decode escapes and are not read. The report is on
|
|
25
|
+
* the whole string, once per string.
|
|
26
|
+
*/
|
|
27
|
+
import { createMultiFeatureCompatRule } from './create-compat-rule.js';
|
|
28
|
+
/** The backslash that starts every escape. */
|
|
29
|
+
const BACKSLASH = '\\';
|
|
30
|
+
/** The unicode escapes of a `$'…'` string's text, in order. */
|
|
31
|
+
function unicodeEscapes(text) {
|
|
32
|
+
const found = [];
|
|
33
|
+
let index = 0;
|
|
34
|
+
while (index < text.length) {
|
|
35
|
+
if (text[index] !== BACKSLASH) {
|
|
36
|
+
index++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const letter = text[index + 1];
|
|
40
|
+
if (letter === 'c') {
|
|
41
|
+
index += 3;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const maxDigits = letter === 'u' ? 4 : letter === 'U' ? 8 : 0;
|
|
45
|
+
if (maxDigits === 0) {
|
|
46
|
+
index += 2;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const digits = /^[0-9A-Fa-f]+/u.exec(text.slice(index + 2, index + 2 + maxDigits))?.[0];
|
|
50
|
+
if (digits === undefined) {
|
|
51
|
+
index += 2;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const length = 2 + digits.length;
|
|
55
|
+
found.push({ text: text.slice(index, index + length), codePoint: parseInt(digits, 16) });
|
|
56
|
+
index += length;
|
|
57
|
+
}
|
|
58
|
+
return found;
|
|
59
|
+
}
|
|
60
|
+
/** Whether a code point is one the Bash 4.2 release writes as a Latin-1 byte. */
|
|
61
|
+
function isLatin1Upper(codePoint) {
|
|
62
|
+
return codePoint >= 0x80 && codePoint <= 0xff;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The UTF-8 bytes of `codePoint` as octal escapes (`\303\251`), or
|
|
66
|
+
* `undefined` for a value that is not a Unicode scalar value.
|
|
67
|
+
*/
|
|
68
|
+
function octalBytes(codePoint) {
|
|
69
|
+
if (codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff))
|
|
70
|
+
return undefined;
|
|
71
|
+
const bytes = new TextEncoder().encode(String.fromCodePoint(codePoint));
|
|
72
|
+
return [...bytes].map((byte) => BACKSLASH + byte.toString(8).padStart(3, '0')).join('');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The `sh/compat/no-unicode-escape` rule module.
|
|
76
|
+
*
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
export const noUnicodeEscape = createMultiFeatureCompatRule({
|
|
80
|
+
name: 'compat/no-unicode-escape',
|
|
81
|
+
features: ['unicodeEscape', 'unicodeEscapeLatin1'],
|
|
82
|
+
meta: {
|
|
83
|
+
type: 'problem',
|
|
84
|
+
docs: {
|
|
85
|
+
description: "Disallow `\\u`/`\\U` escapes in `$'…'` strings when the target shell does not decode them to UTF-8",
|
|
86
|
+
},
|
|
87
|
+
messages: {
|
|
88
|
+
unicodeEscape: "`{{escape}}` in `$'…'` needs {{required}}, but this file targets {{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 targets {{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
|
+
},
|
|
91
|
+
},
|
|
92
|
+
create(context, gates) {
|
|
93
|
+
const escapeGate = gates.get('unicodeEscape');
|
|
94
|
+
const latin1Gate = gates.get('unicodeEscapeLatin1');
|
|
95
|
+
return {
|
|
96
|
+
SglQuoted(node) {
|
|
97
|
+
if (!node.dollar)
|
|
98
|
+
return;
|
|
99
|
+
const escapes = unicodeEscapes(node.value);
|
|
100
|
+
const latin1 = escapes.find((escape) => isLatin1Upper(escape.codePoint));
|
|
101
|
+
// The escape the message names, and the gate it needs: a Latin-1
|
|
102
|
+
// escape needs the newer release, so it decides when present.
|
|
103
|
+
const shown = latin1 ?? escapes.at(0);
|
|
104
|
+
const gate = latin1 === undefined ? escapeGate : latin1Gate;
|
|
105
|
+
if (shown === undefined || gate === undefined)
|
|
106
|
+
return;
|
|
107
|
+
const messageId = escapeGate === undefined ? 'unicodeEscapeLatin1' : 'unicodeEscape';
|
|
108
|
+
context.report({
|
|
109
|
+
node,
|
|
110
|
+
messageId,
|
|
111
|
+
data: {
|
|
112
|
+
...gate,
|
|
113
|
+
escape: shown.text,
|
|
114
|
+
octal: octalBytes(shown.codePoint) ?? '\\NNN',
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export type NoWaitNMessageId = 'waitNext';
|
|
7
|
+
/**
|
|
8
|
+
* The `sh/compat/no-wait-n` rule module.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const noWaitN: import("../define-shell-rule.js").ShellRule<[], "waitNext">;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-wait-n`: reports `wait -n` in files whose target shell lacks
|
|
3
|
+
* it, that is, Bash older than 4.3 (macOS's stock `/bin/bash` is 3.2) and
|
|
4
|
+
* POSIX sh.
|
|
5
|
+
*
|
|
6
|
+
* Bash 4.2 and older reject `-n` as an invalid option and return 2 at once,
|
|
7
|
+
* so a job pool that throttles itself with `wait -n` stops waiting and runs
|
|
8
|
+
* every job together. The message suggests the portable alternative: keep
|
|
9
|
+
* each job's `$!` and wait for one job at a time with `wait "$pid"`, which
|
|
10
|
+
* returns that job's exit status.
|
|
11
|
+
*
|
|
12
|
+
* What is read: a `wait` command (after transparent wrappers such as
|
|
13
|
+
* `builtin`) whose option words, up to the first operand or `--`, include a
|
|
14
|
+
* cluster containing `n` (`-n`, `-fn`). The value after a `-p` cluster
|
|
15
|
+
* (Bash 5.1's `wait -p var`) is its operand, not an option. The report is on
|
|
16
|
+
* the option word.
|
|
17
|
+
*/
|
|
18
|
+
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
19
|
+
import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
|
|
20
|
+
import { createCompatRule } from './create-compat-rule.js';
|
|
21
|
+
/**
|
|
22
|
+
* The wrappers followed to find the `wait` command: `sh-ast/analyze`'s
|
|
23
|
+
* defaults (which include `command`) plus Bash's `builtin`.
|
|
24
|
+
*/
|
|
25
|
+
const TRANSPARENT_WRAPPERS = [
|
|
26
|
+
...DEFAULT_TRANSPARENT_WRAPPERS,
|
|
27
|
+
BUILTIN_WRAPPER,
|
|
28
|
+
];
|
|
29
|
+
/** An option cluster of `wait`: letters after a single `-`. */
|
|
30
|
+
const OPTION_CLUSTER = /^-[A-Za-z]+$/u;
|
|
31
|
+
/**
|
|
32
|
+
* The `sh/compat/no-wait-n` rule module.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export const noWaitN = createCompatRule({
|
|
37
|
+
name: 'compat/no-wait-n',
|
|
38
|
+
feature: 'waitNext',
|
|
39
|
+
meta: {
|
|
40
|
+
type: 'problem',
|
|
41
|
+
docs: {
|
|
42
|
+
description: 'Disallow `wait -n` when the target shell lacks it',
|
|
43
|
+
},
|
|
44
|
+
messages: {
|
|
45
|
+
waitNext: '`wait -n` needs {{required}}, but this file targets {{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
|
+
},
|
|
48
|
+
create(context, gate) {
|
|
49
|
+
const { sourceCode } = context;
|
|
50
|
+
return {
|
|
51
|
+
CallExpr(node) {
|
|
52
|
+
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
53
|
+
transparentWrappers: TRANSPARENT_WRAPPERS,
|
|
54
|
+
});
|
|
55
|
+
if (invoked?.effective.static !== true || invoked.effective.text !== 'wait')
|
|
56
|
+
return;
|
|
57
|
+
// `chainIndices` index `node.args` directly, so slice before filtering.
|
|
58
|
+
const words = node.args.slice((invoked.chainIndices.at(-1) ?? 0) + 1);
|
|
59
|
+
for (let index = 0; index < words.length; index++) {
|
|
60
|
+
const text = sourceCode.resolveWordText(words[index]);
|
|
61
|
+
if (text === null || !OPTION_CLUSTER.test(text))
|
|
62
|
+
return;
|
|
63
|
+
if (text.includes('n')) {
|
|
64
|
+
context.report({ node: words[index], messageId: 'waitNext', data: { ...gate } });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (text.includes('p'))
|
|
68
|
+
index++;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ShellSourceCode } from '../../language/shell-source-code.js';
|
|
2
|
+
import type { ShellNode } from '../../language/syntax-view.js';
|
|
3
|
+
/**
|
|
4
|
+
* The `[start, end)` UTF-16 range of `operator` at `node`'s `OpPos`, or
|
|
5
|
+
* `undefined` when the record has no valid `OpPos` or the text there is not
|
|
6
|
+
* `operator`.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export declare function operatorRange(sourceCode: ShellSourceCode, node: ShellNode, operator: string): [number, number] | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `[start, end)` UTF-16 range of `operator` at `node`'s `OpPos`, or
|
|
3
|
+
* `undefined` when the record has no valid `OpPos` or the text there is not
|
|
4
|
+
* `operator`.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export function operatorRange(sourceCode, node, operator) {
|
|
9
|
+
const syntax = sourceCode.getSyntax(node);
|
|
10
|
+
const pos = syntax?.OpPos;
|
|
11
|
+
if (pos?.IsValid !== true)
|
|
12
|
+
return undefined;
|
|
13
|
+
const start = sourceCode.getIndexFromSyntaxPos(pos);
|
|
14
|
+
return sourceCode.text.startsWith(operator, start) ? [start, start + operator.length] : undefined;
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ShellSourceCode } from '../../language/shell-source-code.js';
|
|
2
|
+
import type { ShellNode, ShellNodeOf } from '../../language/syntax-view.js';
|
|
3
|
+
/**
|
|
4
|
+
* The word that names `command` in `node`, when `command` is what `node`
|
|
5
|
+
* effectively runs and a shell without it can reach `node` (see the module
|
|
6
|
+
* comment), or `undefined` otherwise. Reporting this word rather than the whole
|
|
7
|
+
* command matters for `[[`: the parser ends that command at the first `&&` or
|
|
8
|
+
* `||`, so the node would cover an arbitrary part of the expression.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function reachableCommandWord(sourceCode: ShellSourceCode, node: ShellNodeOf<'CallExpr'>, command: string): ShellNode | undefined;
|