@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
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rule's single options object. It mirrors `no-undeclared-commands`'
|
|
3
|
-
* options, so a project configures both rules with the same wrappers and
|
|
4
|
-
* they compute the same usage.
|
|
5
|
-
*
|
|
6
|
-
* @internal
|
|
7
|
-
*/
|
|
8
|
-
export interface NoUnusedDeclarationsOptions {
|
|
9
|
-
/**
|
|
10
|
-
* Names of project-specific transparent wrappers (`with-retry`), added to
|
|
11
|
-
* the default wrapper table (see `harvestWrapperTable`). The word after a
|
|
12
|
-
* listed wrapper is the command it runs, so `with-retry curl` uses `curl`
|
|
13
|
-
* as well as `with-retry`, and `with-retry "$tool"` runs a command the
|
|
14
|
-
* file cannot name.
|
|
15
|
-
*/
|
|
16
|
-
readonly transparentWrappers?: readonly string[];
|
|
17
|
-
}
|
|
18
1
|
/**
|
|
19
2
|
* The `sh/no-unused-declarations` rule module.
|
|
20
3
|
*
|
|
21
4
|
* @internal
|
|
22
5
|
*/
|
|
23
|
-
export declare const noUnusedDeclarations: import("./define-shell-rule.js").ShellRule<[
|
|
6
|
+
export declare const noUnusedDeclarations: import("./define-shell-rule.js").ShellRule<[], "unusedDeclaration" | "unusedFileDeclaration" | "unusedDescribedDeclaration">;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveTransparentWrappers } from './resolve-transparent-wrappers.js';
|
|
2
2
|
import { harvestCommands } from '../command-harvest/harvest-commands.js';
|
|
3
3
|
import { harvestWrapperTable } from '../command-harvest/wrapper-table.js';
|
|
4
4
|
import { createRule } from './create-rule.js';
|
|
5
5
|
import { readDeclaredCommands } from '../declaration-conventions/read-declared-commands.js';
|
|
6
6
|
import { resolveDeclarationConvention } from './resolve-declaration-convention.js';
|
|
7
|
-
import { networkWrapperSpec } from './resolve-network-wrapper.js';
|
|
8
7
|
import { SHELL_WRAPPER } from './transparent-wrappers.js';
|
|
9
8
|
/** The registry name of this rule (`sh/no-unused-declarations` in a user's config). */
|
|
10
9
|
const RULE_NAME = 'no-unused-declarations';
|
|
@@ -54,29 +53,17 @@ function trapSetsCodeString(operands) {
|
|
|
54
53
|
const SHELL_NAMES = new Set(SHELL_WRAPPER.names);
|
|
55
54
|
/** The builtins that run another file's code in this shell: POSIX `.` and Bash's `source`. */
|
|
56
55
|
const SOURCE_COMMANDS = new Set(['.', 'source']);
|
|
57
|
-
/**
|
|
58
|
-
* The wrappers whose path forms (`/bin/bash`, `/usr/bin/env`) the completeness
|
|
59
|
-
* check also follows, by basename: shells invoked as commands and `env`,
|
|
60
|
-
* the wrappers that can run an embedded command string. sh-ast matches
|
|
61
|
-
* wrapper names exactly, so without this `/bin/bash -c '…'` would resolve to
|
|
62
|
-
* the static command `/bin/bash`.
|
|
63
|
-
*/
|
|
64
|
-
const PATH_FOLLOWED_WRAPPERS = [
|
|
65
|
-
SHELL_WRAPPER,
|
|
66
|
-
...DEFAULT_TRANSPARENT_WRAPPERS.filter((spec) => spec.names.includes('env')),
|
|
67
|
-
];
|
|
68
56
|
/** The basename of `text` when it is a path (contains a slash), else `null`. */
|
|
69
57
|
function pathBasename(text) {
|
|
70
58
|
const slash = text.lastIndexOf('/');
|
|
71
59
|
return slash === -1 ? null : text.slice(slash + 1);
|
|
72
60
|
}
|
|
73
61
|
/**
|
|
74
|
-
* The effective command of `site` under
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* path, and a site has finitely many words, so the loop ends.
|
|
62
|
+
* The effective command of `site` under the resolved completeness `table`.
|
|
63
|
+
* Relative utility paths use only specs already in that table, including its
|
|
64
|
+
* shell-interpreter entries. Recreating a built-in spec here would undo a
|
|
65
|
+
* project's exact-name override and make completeness disagree with usage.
|
|
66
|
+
* Each round adds a new path from the finite argv, so the loop ends.
|
|
80
67
|
*/
|
|
81
68
|
function resolveEffective(site, table) {
|
|
82
69
|
let current = table;
|
|
@@ -89,7 +76,7 @@ function resolveEffective(site, table) {
|
|
|
89
76
|
const base = pathBasename(path);
|
|
90
77
|
if (base === null || current.some((spec) => spec.names.includes(path)))
|
|
91
78
|
return resolution;
|
|
92
|
-
const spec =
|
|
79
|
+
const spec = current.find((entry) => entry.shellBuiltin !== true && entry.names.includes(base));
|
|
93
80
|
if (spec === undefined)
|
|
94
81
|
return resolution;
|
|
95
82
|
current = [{ ...spec, names: [path] }, ...current];
|
|
@@ -177,19 +164,7 @@ export const noUnusedDeclarations = createRule({
|
|
|
177
164
|
description: 'Disallow declaring external commands that a script never uses, per the configured declaration convention',
|
|
178
165
|
},
|
|
179
166
|
fixable: 'code',
|
|
180
|
-
schema: [
|
|
181
|
-
{
|
|
182
|
-
type: 'object',
|
|
183
|
-
properties: {
|
|
184
|
-
transparentWrappers: {
|
|
185
|
-
type: 'array',
|
|
186
|
-
items: { type: 'string', minLength: 1 },
|
|
187
|
-
uniqueItems: true,
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
additionalProperties: false,
|
|
191
|
-
},
|
|
192
|
-
],
|
|
167
|
+
schema: [],
|
|
193
168
|
messages: {
|
|
194
169
|
unusedDeclaration: "'{{command}}' is declared but never used. Remove the declaration so the script's stated dependencies match what it runs.",
|
|
195
170
|
unusedFileDeclaration: "'{{command}}' is declared in {{source}} but this script never uses it. Remove it there so the script's stated dependencies match what it runs.",
|
|
@@ -202,7 +177,7 @@ export const noUnusedDeclarations = createRule({
|
|
|
202
177
|
return {};
|
|
203
178
|
}
|
|
204
179
|
const { sourceCode } = context;
|
|
205
|
-
const transparentWrappers =
|
|
180
|
+
const transparentWrappers = resolveTransparentWrappers(context, harvestWrapperTable(sourceCode.dialect));
|
|
206
181
|
// The completeness check follows the harvest's table plus shells
|
|
207
182
|
// invoked as commands, so `bash -c '…'` and `sh -c '…'` resolve to an
|
|
208
183
|
// embedded command (as in `no-dynamic-invocation`) instead of stopping
|
|
@@ -165,12 +165,13 @@ export const preferLocal = createRule({
|
|
|
165
165
|
},
|
|
166
166
|
create(context) {
|
|
167
167
|
const { sourceCode } = context;
|
|
168
|
-
// `local` is not POSIX: under `posix` there is nothing to declare with.
|
|
169
|
-
if (sourceCode.dialect === 'posix')
|
|
170
|
-
return {};
|
|
171
168
|
const [options] = context.options;
|
|
172
169
|
const allowed = new Set(options.allow ?? []);
|
|
173
170
|
const allowPattern = compileUserPattern('sh/prefer-local', 'allowPattern', options.allowPattern ?? DEFAULT_ALLOW_PATTERN, 'u');
|
|
171
|
+
// Validate configuration in every dialect, even where the rule is inert.
|
|
172
|
+
// `local` is not POSIX: under `posix` there is nothing to declare with.
|
|
173
|
+
if (sourceCode.dialect === 'posix')
|
|
174
|
+
return {};
|
|
174
175
|
const text = sourceCode.text;
|
|
175
176
|
const lastpipe = enablesLastpipe(sourceCode.ast, sourceCode);
|
|
176
177
|
// Names assigned or declared outside every function, with where each
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
import { DashHdoc, Hdoc } from 'sh-ast/analyze';
|
|
42
42
|
import { createRule } from './create-rule.js';
|
|
43
|
+
import { isBashIntegerVariable } from './bash-variable-semantics.js';
|
|
43
44
|
import { messageExcerpt } from '../language/line-breaks.js';
|
|
44
45
|
/** The registry name of this rule (`sh/quote-expansions` in a user's config). */
|
|
45
46
|
const RULE_NAME = 'quote-expansions';
|
|
@@ -50,33 +51,7 @@ const RULE_NAME = 'quote-expansions';
|
|
|
50
51
|
* their values are arbitrary text.
|
|
51
52
|
*/
|
|
52
53
|
const SINGLE_WORD_SPECIALS = new Set(['#', '?', '$', '!', '-']);
|
|
53
|
-
/**
|
|
54
|
-
* Bash variables that always expand to a decimal integer, so an unquoted
|
|
55
|
-
* reference is one word with no glob characters, like the specials above.
|
|
56
|
-
* Bash keeps them integers: assigning text to `RANDOM`, `SECONDS` or `OPTIND`
|
|
57
|
-
* evaluates it arithmetically, `LINENO` and `BASH_SUBSHELL` ignore
|
|
58
|
-
* assignments, and `PPID` is read-only; a hostile value in the environment is
|
|
59
|
-
* ignored for each of them. Each exists in every Bash from 3.2, the oldest
|
|
60
|
-
* target eslint-sh supports.
|
|
61
|
-
*
|
|
62
|
-
* Left out on purpose: `UID` and `EUID`, which Bash imports verbatim from the
|
|
63
|
-
* environment (`env UID='a *' bash -c 'echo "[$UID]"'` prints `[a *]`), so
|
|
64
|
-
* they can hold text that splits and globs; `BASHPID` (Bash 4.0), `EPOCHSECONDS` (5.0) and
|
|
65
|
-
* `SRANDOM` (5.1), which an older Bash leaves unset, so an unquoted reference
|
|
66
|
-
* there expands to no word at all; `EPOCHREALTIME`, whose decimal separator
|
|
67
|
-
* follows the locale; and variables a script or the environment may set to
|
|
68
|
-
* any text (`COLUMNS`, `SHLVL`). The list does not apply outside Bash: in a
|
|
69
|
-
* POSIX shell such as dash these are ordinary variables.
|
|
70
|
-
*/
|
|
71
|
-
const BASH_INTEGER_VARIABLES = new Set([
|
|
72
|
-
'RANDOM',
|
|
73
|
-
'SECONDS',
|
|
74
|
-
'LINENO',
|
|
75
|
-
'PPID',
|
|
76
|
-
'OPTIND',
|
|
77
|
-
'BASH_SUBSHELL',
|
|
78
|
-
]);
|
|
79
|
-
/** The dialects whose scripts run under Bash, where {@link BASH_INTEGER_VARIABLES} holds. */
|
|
54
|
+
/** The dialects whose scripts run under Bash, where {@link isBashIntegerVariable} holds. */
|
|
80
55
|
const BASH_DIALECTS = new Set(['bash', 'bats']);
|
|
81
56
|
/** Redirection operators that introduce a here-document (`<<`, `<<-`). */
|
|
82
57
|
const HEREDOC_OPERATORS = new Set([Hdoc, DashHdoc]);
|
|
@@ -94,7 +69,7 @@ function isSingleWordParamExp(node, bashIntegers) {
|
|
|
94
69
|
}
|
|
95
70
|
const name = node.param?.value;
|
|
96
71
|
return (name !== undefined &&
|
|
97
|
-
(SINGLE_WORD_SPECIALS.has(name) || (bashIntegers &&
|
|
72
|
+
(SINGLE_WORD_SPECIALS.has(name) || (bashIntegers && isBashIntegerVariable(name))) &&
|
|
98
73
|
!node.excl &&
|
|
99
74
|
node.exp === undefined &&
|
|
100
75
|
node.repl === undefined &&
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `sh/require-network-wrapper`: every network command must be run through the
|
|
3
|
-
* project's configured wrapper (a retry, audit, or proxy helper).
|
|
4
|
-
*
|
|
5
|
-
* This is the built-in instance of `createWrapperRule`: the commands
|
|
6
|
-
* default to common network clients, and the wrapper comes from the user's
|
|
7
|
-
* rule's `wrapper` option, or else from the shared `settings.sh.networkWrapper`
|
|
8
|
-
* (which the declaration rules also read). The wrapper has **no default**.
|
|
9
|
-
* Which wrapper guards network calls is a project's policy, and a generic
|
|
10
|
-
* linter must not invent it, so with neither set the rule is inert: it
|
|
11
|
-
* registers no listeners, reports nothing, and never analyzes the file.
|
|
12
|
-
*
|
|
13
|
-
* `git` counts only for the subcommands that talk to a remote (`fetch`,
|
|
14
|
-
* `clone`, `pull`, `push`), found after git's own global options
|
|
15
|
-
* (`git -C dir fetch`).
|
|
16
|
-
*/
|
|
17
1
|
import type { WrapperSpec } from 'sh-ast/analyze';
|
|
18
2
|
/**
|
|
19
3
|
* The options of `sh/require-network-wrapper`.
|
|
@@ -26,7 +10,8 @@ export interface RequireNetworkWrapperOptions {
|
|
|
26
10
|
/**
|
|
27
11
|
* The wrapper network commands must be run through: a name, or a
|
|
28
12
|
* `WrapperSpec` declaring the wrapper's option shape. Unset falls back to
|
|
29
|
-
* `settings
|
|
13
|
+
* `settings: { sh: { networkWrapper: 'with-retry' } }`; conflicting values
|
|
14
|
+
* throw instead of overriding the shared policy. With neither, the rule is inert.
|
|
30
15
|
*/
|
|
31
16
|
readonly wrapper?: string | WrapperSpec;
|
|
32
17
|
/** Commands checked in addition to `commands` (or its default). */
|
|
@@ -1,6 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/require-network-wrapper`: every network command must be run through the
|
|
3
|
+
* project's configured wrapper (a retry, audit, or proxy helper).
|
|
4
|
+
*
|
|
5
|
+
* This is the built-in instance of `createWrapperRule`: the commands
|
|
6
|
+
* default to common network clients, and the wrapper comes from the user's
|
|
7
|
+
* shared `settings: { sh: { networkWrapper: 'with-retry' } }` or the local
|
|
8
|
+
* `wrapper` option. Conflicting values throw, so policies cannot silently disagree. The wrapper has **no default**.
|
|
9
|
+
* Which wrapper guards network calls is a project's policy, and a generic
|
|
10
|
+
* linter must not invent it, so with neither set the rule is inert: it
|
|
11
|
+
* registers no listeners, reports nothing, and never analyzes the file.
|
|
12
|
+
*
|
|
13
|
+
* `git` counts only for the subcommands that talk to a remote (`fetch`,
|
|
14
|
+
* `clone`, `pull`, `push`), found after git's own global options
|
|
15
|
+
* (`git -C dir fetch`).
|
|
16
|
+
*/
|
|
17
|
+
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
1
18
|
import { createRule } from './create-rule.js';
|
|
2
19
|
import { WRAPPER_RULE_MESSAGES, buildWrapperRule } from './create-wrapper-rule.js';
|
|
3
|
-
import { readNetworkWrapperSetting } from './resolve-network-wrapper.js';
|
|
20
|
+
import { networkWrapperSpec, readNetworkWrapperSetting, sameWrapperSpec, } from './resolve-network-wrapper.js';
|
|
4
21
|
/** The registry name of this rule (`sh/require-network-wrapper` in a user's config). */
|
|
5
22
|
const RULE_NAME = 'require-network-wrapper';
|
|
6
23
|
/**
|
|
@@ -182,6 +199,7 @@ const WRAPPER_SPEC_SCHEMA = {
|
|
|
182
199
|
stopsChainFlags: STRING_LIST_SCHEMA,
|
|
183
200
|
positionalOperandsBeforeCommand: { type: 'integer', minimum: 0 },
|
|
184
201
|
assignmentOperands: { enum: ['shell-identifier', 'env', 'sudo'] },
|
|
202
|
+
shellBuiltin: { type: 'boolean' },
|
|
185
203
|
},
|
|
186
204
|
required: ['names'],
|
|
187
205
|
additionalProperties: false,
|
|
@@ -191,9 +209,20 @@ const WRAPPER_SPEC_SCHEMA = {
|
|
|
191
209
|
* `null` when no wrapper is configured.
|
|
192
210
|
*/
|
|
193
211
|
function wrapperRuleConfig(options, settings) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
const shared = readNetworkWrapperSetting(settings);
|
|
213
|
+
if (options?.wrapper !== undefined && shared !== undefined) {
|
|
214
|
+
const configured = options.wrapper;
|
|
215
|
+
const option = typeof configured === 'string'
|
|
216
|
+
? (DEFAULT_TRANSPARENT_WRAPPERS.find((spec) => spec.names.includes(configured)) ?? {
|
|
217
|
+
names: [configured],
|
|
218
|
+
})
|
|
219
|
+
: configured;
|
|
220
|
+
const network = networkWrapperSpec(settings);
|
|
221
|
+
if (network !== null && !sameWrapperSpec(option, network)) {
|
|
222
|
+
throw new Error('sh/require-network-wrapper: `wrapper` conflicts with the shared networkWrapper; configure one shared network policy.');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const wrapper = shared ?? options?.wrapper;
|
|
197
226
|
if (wrapper === undefined)
|
|
198
227
|
return null;
|
|
199
228
|
return {
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
* so a dynamic command word is never matched.
|
|
26
26
|
*
|
|
27
27
|
* The suggestion wraps the offending statement in `if [ -t 0 ]; then … fi`.
|
|
28
|
+
* It is withheld when another command shares the line that opens the
|
|
29
|
+
* statement's here-document, because the statement's source range runs on to
|
|
30
|
+
* that here-document's terminator and would carry the other command into the
|
|
31
|
+
* guard.
|
|
28
32
|
*/
|
|
29
33
|
import { DashHdoc, Hdoc } from 'sh-ast/analyze';
|
|
30
34
|
import { createRule } from './create-rule.js';
|
|
@@ -147,6 +151,26 @@ function isControlFlowOperand(stmt, sourceCode) {
|
|
|
147
151
|
}
|
|
148
152
|
/** Redirection operators that open a here-document (`<<`, `<<-`), whose body follows the line. */
|
|
149
153
|
const HEREDOC_OPERATORS = new Set([Hdoc, DashHdoc]);
|
|
154
|
+
/**
|
|
155
|
+
* Whether other source shares the line that opens one of `stmt`'s
|
|
156
|
+
* here-documents. A here-document's body follows that line, so the statement's
|
|
157
|
+
* range reaches the body's terminator and covers whatever else sits on the
|
|
158
|
+
* opener line (`read x 3<<EOF; echo hi`, a trailing comment): wrapping the
|
|
159
|
+
* range would put that source inside the guard, where it runs only on a
|
|
160
|
+
* terminal. A bare `;` after the opener belongs to the statement and does not
|
|
161
|
+
* count. The opener line is read from the end of the statement's inline
|
|
162
|
+
* tokens (its command, its redirections' operators and words), so the
|
|
163
|
+
* statement's own further redirections on that line are not foreign.
|
|
164
|
+
*/
|
|
165
|
+
function sharesHeredocOpenerLine(stmt, text) {
|
|
166
|
+
const heredocs = stmt.redirs.filter((redir) => HEREDOC_OPERATORS.has(redir.op));
|
|
167
|
+
if (heredocs.length === 0)
|
|
168
|
+
return false;
|
|
169
|
+
const inlineEnd = Math.max(...(stmt.cmd === undefined ? [] : [stmt.cmd.range[1]]), ...stmt.redirs.map((redir) => HEREDOC_OPERATORS.has(redir.op) ? (redir.word?.range[1] ?? redir.range[1]) : redir.range[1]));
|
|
170
|
+
const lineEnd = text.indexOf('\n', inlineEnd);
|
|
171
|
+
const tail = text.slice(inlineEnd, lineEnd === -1 ? text.length : lineEnd);
|
|
172
|
+
return !/^[ \t]*;?[ \t]*\r?$/u.test(tail);
|
|
173
|
+
}
|
|
150
174
|
/**
|
|
151
175
|
* The text that replaces `stmtText` (a statement starting at `start`) with
|
|
152
176
|
* the statement inside `if [ -t 0 ]; then … fi`.
|
|
@@ -261,7 +285,12 @@ export const requireTtyGuard = createRule({
|
|
|
261
285
|
const stmt = sourceCode.getParent(node);
|
|
262
286
|
// A statement that runs in the background cannot be wrapped without
|
|
263
287
|
// changing what is backgrounded, so it gets no suggestion.
|
|
264
|
-
|
|
288
|
+
// A statement sharing its here-document's opener line with another
|
|
289
|
+
// command gets none either (see `sharesHeredocOpenerLine`).
|
|
290
|
+
const wrappable = stmt?.type === 'Stmt' &&
|
|
291
|
+
!stmt.background &&
|
|
292
|
+
!isControlFlowOperand(stmt, sourceCode) &&
|
|
293
|
+
!sharesHeredocOpenerLine(stmt, sourceCode.text);
|
|
265
294
|
context.report({
|
|
266
295
|
node,
|
|
267
296
|
messageId: 'unguardedInteractive',
|
|
@@ -52,16 +52,15 @@ export interface DeclarationConventionBindingContext {
|
|
|
52
52
|
/**
|
|
53
53
|
* Resolves the declaration convention bound for the rule running in
|
|
54
54
|
* `context`: the rule's `declarationConvention` option, else
|
|
55
|
-
* `settings
|
|
55
|
+
* `settings: { sh: { declarationConvention: convention } }`, else `null`
|
|
56
|
+
* (the rule is inert).
|
|
56
57
|
*
|
|
57
58
|
* Throws an `Error` naming the offending setting when the configured value is
|
|
58
59
|
* present but does not implement `declaredCommands` (or has a non-function
|
|
59
60
|
* `removeDeclaration`). The message names where the value came from and what
|
|
60
|
-
* it was,
|
|
61
|
-
* `
|
|
62
|
-
*
|
|
63
|
-
* surfaces as ESLint's
|
|
64
|
-
* configuration message for the file being linted.
|
|
61
|
+
* it was, including the expected convention shape and the received value.
|
|
62
|
+
* Call it from `create()`, so the error surfaces as ESLint's configuration
|
|
63
|
+
* message for the file being linted.
|
|
65
64
|
*
|
|
66
65
|
* A rule that receives `null` should register no listeners: with no
|
|
67
66
|
* convention bound, a convention consumer has nothing to check.
|
|
@@ -51,16 +51,15 @@ function assertDeclarationConvention(value, setting) {
|
|
|
51
51
|
/**
|
|
52
52
|
* Resolves the declaration convention bound for the rule running in
|
|
53
53
|
* `context`: the rule's `declarationConvention` option, else
|
|
54
|
-
* `settings
|
|
54
|
+
* `settings: { sh: { declarationConvention: convention } }`, else `null`
|
|
55
|
+
* (the rule is inert).
|
|
55
56
|
*
|
|
56
57
|
* Throws an `Error` naming the offending setting when the configured value is
|
|
57
58
|
* present but does not implement `declaredCommands` (or has a non-function
|
|
58
59
|
* `removeDeclaration`). The message names where the value came from and what
|
|
59
|
-
* it was,
|
|
60
|
-
* `
|
|
61
|
-
*
|
|
62
|
-
* surfaces as ESLint's
|
|
63
|
-
* configuration message for the file being linted.
|
|
60
|
+
* it was, including the expected convention shape and the received value.
|
|
61
|
+
* Call it from `create()`, so the error surfaces as ESLint's configuration
|
|
62
|
+
* message for the file being linted.
|
|
64
63
|
*
|
|
65
64
|
* A rule that receives `null` should register no listeners: with no
|
|
66
65
|
* convention bound, a convention consumer has nothing to check.
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { WrapperSpec } from 'sh-ast/analyze';
|
|
2
|
+
/**
|
|
3
|
+
* Checks a configured value and returns it typed: a non-empty command name,
|
|
4
|
+
* or a `WrapperSpec` object made of the JSON-expressible fields.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export declare function validateWrapperSetting(value: unknown, setting: string): string | WrapperSpec;
|
|
2
8
|
/**
|
|
3
9
|
* Reads `settings.sh.networkWrapper`: the configured name or `WrapperSpec`,
|
|
4
10
|
* or `undefined` when it is not set. Throws an error that starts
|
|
@@ -17,3 +23,16 @@ export declare function readNetworkWrapperSetting(settings: Readonly<Record<stri
|
|
|
17
23
|
* @internal
|
|
18
24
|
*/
|
|
19
25
|
export declare function networkWrapperSpec(settings: Readonly<Record<string, unknown>>): WrapperSpec | null;
|
|
26
|
+
/**
|
|
27
|
+
* Whether overlapping wrapper names receive the same effective matcher grammar.
|
|
28
|
+
* Extra aliases are independent facts, not a conflict for the shared names.
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export declare function sameWrapperOptions(left: WrapperSpec, right: WrapperSpec): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Whether both specs approve the same names with the same effective grammar.
|
|
34
|
+
* Network policy compares the full name set: a transparent alias alone must
|
|
35
|
+
* not become an approved network wrapper. Name and flag order do not matter.
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export declare function sameWrapperSpec(left: WrapperSpec, right: WrapperSpec): boolean;
|
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* whose findings depend on that wrapper reads the same declaration.
|
|
5
|
-
*
|
|
6
|
-
* Two different statements are involved, and this setting makes only one of
|
|
7
|
-
* them:
|
|
8
|
-
*
|
|
9
|
-
* - **Policy:** network commands must run through this wrapper. That is what
|
|
10
|
-
* `sh/require-network-wrapper` enforces, and this setting states it.
|
|
11
|
-
* - **Fact:** this wrapper runs the command named after it. The policy implies
|
|
12
|
-
* the fact (a wrapper that encloses `curl` runs `curl`), so
|
|
13
|
-
* `sh/no-undeclared-commands` and `sh/no-unused-declarations` look through
|
|
14
|
-
* the network wrapper when it is set here. The converse does not hold: a
|
|
15
|
-
* transparent wrapper such as `with-lock` is not the network wrapper, and
|
|
16
|
-
* stays in those rules' `transparentWrappers` option.
|
|
17
|
-
*
|
|
18
|
-
* ESLint gives a rule no way to read another rule's options, so a wrapper set
|
|
19
|
-
* only as `require-network-wrapper`'s `wrapper` option is invisible to the
|
|
20
|
-
* declaration rules. Shared settings are visible to every rule; that is why
|
|
21
|
-
* the one declaration lives here.
|
|
2
|
+
* Reads the shared network policy and validates JSON-expressible wrapper specs.
|
|
3
|
+
* A network wrapper also supplies a transparent-wrapper fact to every consumer.
|
|
22
4
|
*/
|
|
23
5
|
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
24
6
|
/** The setting's dotted path, as error messages name it. */
|
|
@@ -34,21 +16,23 @@ const KNOWN_FIELDS = new Set([
|
|
|
34
16
|
...STRING_LIST_FIELDS,
|
|
35
17
|
'positionalOperandsBeforeCommand',
|
|
36
18
|
'assignmentOperands',
|
|
19
|
+
'shellBuiltin',
|
|
37
20
|
]);
|
|
38
21
|
/** `true` when `value` is an array of non-empty strings. */
|
|
39
22
|
function isStringList(value) {
|
|
40
23
|
return (Array.isArray(value) &&
|
|
41
24
|
value.every((item) => typeof item === 'string' && item.length > 0));
|
|
42
25
|
}
|
|
43
|
-
/** Throws the setting's configuration error with `detail`. */
|
|
44
|
-
function invalid(detail) {
|
|
45
|
-
throw new Error(`Invalid ${SETTING}: ${detail}`);
|
|
46
|
-
}
|
|
47
26
|
/**
|
|
48
27
|
* Checks a configured value and returns it typed: a non-empty command name,
|
|
49
28
|
* or a `WrapperSpec` object made of the JSON-expressible fields.
|
|
29
|
+
* @internal
|
|
50
30
|
*/
|
|
51
|
-
function
|
|
31
|
+
export function validateWrapperSetting(value, setting) {
|
|
32
|
+
/** Names the configuration binding instead of exposing an analysis failure. */
|
|
33
|
+
function invalid(detail) {
|
|
34
|
+
throw new Error(`Invalid ${setting}: ${detail}`);
|
|
35
|
+
}
|
|
52
36
|
if (typeof value === 'string') {
|
|
53
37
|
if (value.length === 0)
|
|
54
38
|
invalid('expected a command name or a wrapper spec, but got "".');
|
|
@@ -71,6 +55,9 @@ function validate(value) {
|
|
|
71
55
|
invalid(`\`${field}\` must be an array of non-empty strings.`);
|
|
72
56
|
}
|
|
73
57
|
}
|
|
58
|
+
if (spec.shellBuiltin !== undefined && typeof spec.shellBuiltin !== 'boolean') {
|
|
59
|
+
invalid('`shellBuiltin` must be a boolean.');
|
|
60
|
+
}
|
|
74
61
|
const positional = spec.positionalOperandsBeforeCommand;
|
|
75
62
|
if (positional !== undefined && !(Number.isInteger(positional) && positional >= 0)) {
|
|
76
63
|
invalid('`positionalOperandsBeforeCommand` must be a non-negative integer.');
|
|
@@ -94,7 +81,7 @@ export function readNetworkWrapperSetting(settings) {
|
|
|
94
81
|
if (typeof sh !== 'object' || sh === null)
|
|
95
82
|
return undefined;
|
|
96
83
|
const value = sh.networkWrapper;
|
|
97
|
-
return value === undefined ? undefined :
|
|
84
|
+
return value === undefined ? undefined : validateWrapperSetting(value, SETTING);
|
|
98
85
|
}
|
|
99
86
|
/**
|
|
100
87
|
* The `WrapperSpec` for the network wrapper set in `settings.sh.networkWrapper`,
|
|
@@ -114,3 +101,47 @@ export function networkWrapperSpec(settings) {
|
|
|
114
101
|
names: [wrapper],
|
|
115
102
|
});
|
|
116
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Canonical effective grammar from sh-ast 0.9.0's WrapperSpec matcher, excluding
|
|
106
|
+
* the names that select it. Flags are membership sets; omitted lists are empty,
|
|
107
|
+
* shellBuiltin defaults to false (utility matching), and positional count
|
|
108
|
+
* defaults to zero. An explicit assignment grammar supersedes the legacy
|
|
109
|
+
* shell-identifier alias; otherwise there is no assignment grammar.
|
|
110
|
+
*
|
|
111
|
+
* Expanded built-ins may contain a flag pattern even though JSON settings may
|
|
112
|
+
* not. Compare its source and flags structurally, including absence, rather
|
|
113
|
+
* than attempting regex-language equivalence or inheriting omitted fields.
|
|
114
|
+
*/
|
|
115
|
+
function canonicalWrapperGrammar(spec) {
|
|
116
|
+
/** Stable membership representation for the matcher's four flag lists. */
|
|
117
|
+
const flags = (values) => [...new Set(values ?? [])].sort();
|
|
118
|
+
const pattern = spec.noArgFlagPattern;
|
|
119
|
+
return JSON.stringify({
|
|
120
|
+
noArgFlags: flags(spec.noArgFlags),
|
|
121
|
+
argFlags: flags(spec.argFlags),
|
|
122
|
+
unresolvableFlags: flags(spec.unresolvableFlags),
|
|
123
|
+
stopsChainFlags: flags(spec.stopsChainFlags),
|
|
124
|
+
shellBuiltin: spec.shellBuiltin ?? false,
|
|
125
|
+
positionalOperandsBeforeCommand: spec.positionalOperandsBeforeCommand ?? 0,
|
|
126
|
+
assignmentOperands: spec.assignmentOperands ?? (spec.skipAssignmentOperands === true ? 'shell-identifier' : null),
|
|
127
|
+
noArgFlagPattern: pattern === undefined ? null : { source: pattern.source, flags: pattern.flags },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Whether overlapping wrapper names receive the same effective matcher grammar.
|
|
132
|
+
* Extra aliases are independent facts, not a conflict for the shared names.
|
|
133
|
+
* @internal
|
|
134
|
+
*/
|
|
135
|
+
export function sameWrapperOptions(left, right) {
|
|
136
|
+
return canonicalWrapperGrammar(left) === canonicalWrapperGrammar(right);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether both specs approve the same names with the same effective grammar.
|
|
140
|
+
* Network policy compares the full name set: a transparent alias alone must
|
|
141
|
+
* not become an approved network wrapper. Name and flag order do not matter.
|
|
142
|
+
* @internal
|
|
143
|
+
*/
|
|
144
|
+
export function sameWrapperSpec(left, right) {
|
|
145
|
+
return (JSON.stringify([...left.names].sort()) === JSON.stringify([...right.names].sort()) &&
|
|
146
|
+
sameWrapperOptions(left, right));
|
|
147
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves project wrapper facts once per consuming rule. Shared facts precede
|
|
3
|
+
* the rule's built-in table, so an explicit option shape replaces the default
|
|
4
|
+
* for that name. Rules retain their distinct built-in scopes, but never their
|
|
5
|
+
* own interpretation of a configured project wrapper.
|
|
6
|
+
*/
|
|
7
|
+
import type { SettingsConfig } from '@eslint/core';
|
|
8
|
+
import type { WrapperSpec } from 'sh-ast/analyze';
|
|
9
|
+
import type { ShellSourceCode } from '../language/shell-source-code.js';
|
|
10
|
+
/** The rule-context fields needed to resolve shared wrappers and their path spellings. @public */
|
|
11
|
+
export interface TransparentWrappersBindingContext {
|
|
12
|
+
/** Shared ESLint configuration containing the project's shell facts. */
|
|
13
|
+
readonly settings: SettingsConfig;
|
|
14
|
+
/** The current file supplies path spellings without filesystem inspection or execution. */
|
|
15
|
+
readonly sourceCode: ShellSourceCode;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Returns the shared project wrappers followed by `builtIns` (defaults when
|
|
19
|
+
* omitted). Entries accept command names or JSON-expressible `WrapperSpec`s
|
|
20
|
+
* in `settings: { sh: { transparentWrappers: [...] } }`. The shared network
|
|
21
|
+
* wrapper is also transparent; conflicting specs for the same project name
|
|
22
|
+
* throw rather than allowing policies to disagree. Shared specs compare effective
|
|
23
|
+
* grammars for overlapping names, normalizing matcher defaults and retaining
|
|
24
|
+
* expanded built-in flag patterns; an extra transparent alias does not acquire
|
|
25
|
+
* network approval. Explicit specs remain complete replacements. Duplicate project
|
|
26
|
+
* names are rejected so list order cannot silently choose their meaning.
|
|
27
|
+
*
|
|
28
|
+
* Utility wrappers match absolute and relative paths by basename. Path words
|
|
29
|
+
* remain unchanged in analysis results. `shellBuiltin: true` entries match
|
|
30
|
+
* exact names only: an executable path cannot acquire in-process semantics.
|
|
31
|
+
* An explicit path spec takes precedence over a basename fallback regardless
|
|
32
|
+
* of configuration order.
|
|
33
|
+
* Aliases are built even when only `builtIns` are supplied. Overridden names
|
|
34
|
+
* are removed from lower-priority entries, preserving their unrelated aliases
|
|
35
|
+
* and preventing a utility entry from undoing an exact-name builtin override.
|
|
36
|
+
* The input tables are never mutated.
|
|
37
|
+
*
|
|
38
|
+
* @param context - The consuming rule's context.
|
|
39
|
+
* @param builtIns - The rule's own built-in wrapper scope.
|
|
40
|
+
* @throws Error when a configured wrapper is malformed or contradictory.
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveTransparentWrappers(context: TransparentWrappersBindingContext, builtIns?: readonly WrapperSpec[]): readonly WrapperSpec[];
|