@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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A gate is data: a feature name mapped to the oldest Bash release that has
|
|
6
6
|
* it and whether POSIX sh has it. A new gate is a new row here, not new
|
|
7
|
-
* decision logic in a rule. The semantics of `languageOptions.
|
|
7
|
+
* decision logic in a rule. The semantics of `languageOptions.minBashVersion`
|
|
8
8
|
* and `languageOptions.dialect` for gating are defined once, by
|
|
9
9
|
* {@link isFeatureAvailable}, and documented for users in `docs/compat.md`.
|
|
10
10
|
*
|
|
@@ -35,28 +35,28 @@ export type CompatFeature = 'appendAllRedirect' | 'associativeArrays' | 'braceSe
|
|
|
35
35
|
*
|
|
36
36
|
* @internal
|
|
37
37
|
*/
|
|
38
|
-
export type GateLanguageOptions = Pick<ShellLanguageOptions, 'dialect' | '
|
|
38
|
+
export type GateLanguageOptions = Pick<ShellLanguageOptions, 'dialect' | 'minBashVersion'>;
|
|
39
39
|
/**
|
|
40
40
|
* Whether `feature` is available to a file linted with `languageOptions`.
|
|
41
41
|
*
|
|
42
42
|
* - `dialect: "posix"`: available only when the gate's `posix` is `true`,
|
|
43
|
-
*
|
|
44
|
-
* - `dialect: "bash"` (the default) or `"bats"`: an unset `
|
|
45
|
-
* means
|
|
46
|
-
* `
|
|
43
|
+
* regardless of Bash releases (a Bash-only feature is never available).
|
|
44
|
+
* - `dialect: "bash"` (the default) or `"bats"`: an unset `minBashVersion`
|
|
45
|
+
* means no Bash version constraint, so every Bash feature is available. A set
|
|
46
|
+
* `minBashVersion` makes the feature available when it is the gate's `bash`
|
|
47
47
|
* minimum or newer, comparing major then minor numerically (`"4"` is
|
|
48
48
|
* `4.0`; `"3.10"` is newer than `"3.2"`). A `null` minimum is available at
|
|
49
49
|
* every version.
|
|
50
50
|
* - `dialect: "mksh"` or `"zsh"`: the table carries no data for these
|
|
51
|
-
* shells
|
|
52
|
-
*
|
|
51
|
+
* shells. Config validation rejects `minBashVersion` for them; no Bash
|
|
52
|
+
* gate applies and every feature is available.
|
|
53
53
|
*
|
|
54
54
|
* @internal
|
|
55
55
|
*/
|
|
56
56
|
export declare function isFeatureAvailable(feature: CompatFeature, languageOptions: GateLanguageOptions): boolean;
|
|
57
57
|
/**
|
|
58
58
|
* Human-readable words for a gate's report: what the feature needs
|
|
59
|
-
* (`required`, such as `bash 4.0 or newer`) and what the file
|
|
59
|
+
* (`required`, such as `bash 4.0 or newer`) and what the file must support
|
|
60
60
|
* (`target`, such as `bash 3.2` or `POSIX sh`). Compat rule messages
|
|
61
61
|
* interpolate both as `{{required}}` and `{{target}}`.
|
|
62
62
|
*
|
|
@@ -151,7 +151,7 @@ function compareVersions(a, b) {
|
|
|
151
151
|
return a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1];
|
|
152
152
|
}
|
|
153
153
|
/**
|
|
154
|
-
* The dialects whose `
|
|
154
|
+
* The dialects whose `minBashVersion` is a Bash version. `bats` test files run
|
|
155
155
|
* under Bash, so they are gated exactly like `bash` files.
|
|
156
156
|
*/
|
|
157
157
|
const BASH_DIALECTS = new Set(['bash', 'bats']);
|
|
@@ -159,16 +159,16 @@ const BASH_DIALECTS = new Set(['bash', 'bats']);
|
|
|
159
159
|
* Whether `feature` is available to a file linted with `languageOptions`.
|
|
160
160
|
*
|
|
161
161
|
* - `dialect: "posix"`: available only when the gate's `posix` is `true`,
|
|
162
|
-
*
|
|
163
|
-
* - `dialect: "bash"` (the default) or `"bats"`: an unset `
|
|
164
|
-
* means
|
|
165
|
-
* `
|
|
162
|
+
* regardless of Bash releases (a Bash-only feature is never available).
|
|
163
|
+
* - `dialect: "bash"` (the default) or `"bats"`: an unset `minBashVersion`
|
|
164
|
+
* means no Bash version constraint, so every Bash feature is available. A set
|
|
165
|
+
* `minBashVersion` makes the feature available when it is the gate's `bash`
|
|
166
166
|
* minimum or newer, comparing major then minor numerically (`"4"` is
|
|
167
167
|
* `4.0`; `"3.10"` is newer than `"3.2"`). A `null` minimum is available at
|
|
168
168
|
* every version.
|
|
169
169
|
* - `dialect: "mksh"` or `"zsh"`: the table carries no data for these
|
|
170
|
-
* shells
|
|
171
|
-
*
|
|
170
|
+
* shells. Config validation rejects `minBashVersion` for them; no Bash
|
|
171
|
+
* gate applies and every feature is available.
|
|
172
172
|
*
|
|
173
173
|
* @internal
|
|
174
174
|
*/
|
|
@@ -179,14 +179,14 @@ export function isFeatureAvailable(feature, languageOptions) {
|
|
|
179
179
|
return gate.posix;
|
|
180
180
|
if (!BASH_DIALECTS.has(dialect))
|
|
181
181
|
return true;
|
|
182
|
-
const {
|
|
183
|
-
if (
|
|
182
|
+
const { minBashVersion } = languageOptions;
|
|
183
|
+
if (minBashVersion === undefined || gate.bash === null)
|
|
184
184
|
return true;
|
|
185
|
-
const
|
|
185
|
+
const floor = parseVersion(minBashVersion);
|
|
186
186
|
const minimum = parseVersion(gate.bash);
|
|
187
|
-
if (
|
|
187
|
+
if (floor === undefined || minimum === undefined)
|
|
188
188
|
return true;
|
|
189
|
-
return compareVersions(
|
|
189
|
+
return compareVersions(floor, minimum) >= 0;
|
|
190
190
|
}
|
|
191
191
|
/**
|
|
192
192
|
* Describes why `feature` is unavailable under `languageOptions`, for a
|
|
@@ -200,6 +200,6 @@ export function describeGate(feature, languageOptions) {
|
|
|
200
200
|
const required = gate.bash === null ? 'bash' : `bash ${gate.bash} or newer`;
|
|
201
201
|
const target = (languageOptions.dialect ?? 'bash') === 'posix'
|
|
202
202
|
? 'POSIX sh'
|
|
203
|
-
: `bash ${languageOptions.
|
|
203
|
+
: `bash ${languageOptions.minBashVersion ?? ''}`.trim();
|
|
204
204
|
return { required, target };
|
|
205
205
|
}
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* this analysis and are out of scope.
|
|
38
38
|
*/
|
|
39
39
|
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
40
|
+
import { resolveTransparentWrappers } from './resolve-transparent-wrappers.js';
|
|
40
41
|
import { definedFunctionNames } from '../command-harvest/harvest-commands.js';
|
|
41
42
|
import { createRule, packageDocsUrl } from './create-rule.js';
|
|
42
43
|
/**
|
|
@@ -187,12 +188,13 @@ function staticText(word) {
|
|
|
187
188
|
* Resolves `site`'s chain through `compiled`'s wrapper table, following a
|
|
188
189
|
* wrapper invoked by path as the wrapper it names.
|
|
189
190
|
*
|
|
190
|
-
* `resolveArgv0`
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* entry, the site is resolved again with that entry also recognized under the
|
|
191
|
+
* `resolveArgv0` follows absolute wrapper paths directly. Relative wrapper
|
|
192
|
+
* paths also count for this policy: when the chain ends at a static word
|
|
193
|
+
* with a slash whose basename names a table
|
|
194
|
+
* utility entry, the site is resolved again with that entry also recognized under the
|
|
194
195
|
* exact path text, so its options and wrapped command are located by the
|
|
195
|
-
* same analysis.
|
|
196
|
+
* same analysis. Shell builtin entries require exact names and never gain
|
|
197
|
+
* path spellings from this policy. Each pass adds one new name drawn from the site's own argv,
|
|
196
198
|
* so the loop ends.
|
|
197
199
|
*/
|
|
198
200
|
function resolveChain(compiled, site) {
|
|
@@ -202,7 +204,7 @@ function resolveChain(compiled, site) {
|
|
|
202
204
|
const last = staticText(resolution.chain.at(-1));
|
|
203
205
|
if (last === null || table.some((spec) => spec.names.includes(last)))
|
|
204
206
|
return resolution;
|
|
205
|
-
const spec = compiled.transparentWrappers.find((entry) => matchName(last, new Set(entry.names)) !== null);
|
|
207
|
+
const spec = compiled.transparentWrappers.find((entry) => entry.shellBuiltin !== true && matchName(last, new Set(entry.names)) !== null);
|
|
206
208
|
if (spec === undefined)
|
|
207
209
|
return resolution;
|
|
208
210
|
table = [{ ...spec, names: [last] }, ...table];
|
|
@@ -365,7 +367,10 @@ export function buildWrapperRule(config) {
|
|
|
365
367
|
schema: [],
|
|
366
368
|
messages: WRAPPER_RULE_MESSAGES,
|
|
367
369
|
},
|
|
368
|
-
create: (context) => wrapperRuleListeners(
|
|
370
|
+
create: (context) => wrapperRuleListeners({
|
|
371
|
+
...compiled,
|
|
372
|
+
transparentWrappers: resolveTransparentWrappers(context, compiled.transparentWrappers),
|
|
373
|
+
}, context),
|
|
369
374
|
});
|
|
370
375
|
}
|
|
371
376
|
/**
|
|
@@ -18,14 +18,6 @@ export interface NoDynamicInvocationOptions {
|
|
|
18
18
|
* `dynamicInvocation`. Embedded commands still report.
|
|
19
19
|
*/
|
|
20
20
|
readonly allowInFunctions?: readonly string[];
|
|
21
|
-
/**
|
|
22
|
-
* Command names to treat as transparent wrappers, in addition to the
|
|
23
|
-
* built-in list: the word after one is the command that runs. Each is
|
|
24
|
-
* followed by name only, with no options of its own, so a flag after it
|
|
25
|
-
* makes the effective command unknown. A name already in the built-in list
|
|
26
|
-
* keeps its built-in option handling.
|
|
27
|
-
*/
|
|
28
|
-
readonly allowWrappers?: readonly string[];
|
|
29
21
|
}
|
|
30
22
|
/**
|
|
31
23
|
* The `sh/no-dynamic-invocation` rule module.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* reports as `dynamicInvocation`.
|
|
23
23
|
* - The wrappers followed are `sh-ast/analyze`'s defaults, Bash's `builtin`,
|
|
24
24
|
* shells invoked as commands (so `bash -c` is an embedded command and
|
|
25
|
-
* `bash script.sh` runs `script.sh`), and the
|
|
25
|
+
* `bash script.sh` runs `script.sh`), and the shared project wrapper facts.
|
|
26
26
|
*
|
|
27
27
|
* The analysis is syntactic by design: `cmd=ls; "$cmd"` reports, because
|
|
28
28
|
* resolving it would need dataflow. Aliases need no handling: they are not
|
|
@@ -31,16 +31,12 @@
|
|
|
31
31
|
* command is a human decision.
|
|
32
32
|
*/
|
|
33
33
|
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
34
|
+
import { resolveTransparentWrappers } from './resolve-transparent-wrappers.js';
|
|
34
35
|
import { createRule } from './create-rule.js';
|
|
35
36
|
import { BUILTIN_WRAPPER, SHELL_WRAPPER } from './transparent-wrappers.js';
|
|
36
37
|
/** The registry name of this rule (`sh/no-dynamic-invocation` in a user's config). */
|
|
37
38
|
const RULE_NAME = 'no-dynamic-invocation';
|
|
38
|
-
/**
|
|
39
|
-
* The wrappers every invocation is followed through before any configured in
|
|
40
|
-
* `allowWrappers`: `sh-ast/analyze`'s defaults, then Bash's `builtin`, then
|
|
41
|
-
* shells invoked as commands. `resolveArgv0` uses the first entry whose name
|
|
42
|
-
* matches, so the built-in entries take precedence over a user's.
|
|
43
|
-
*/
|
|
39
|
+
/** Default scope for dynamic invocations, including builtin and shell entrypoints. */
|
|
44
40
|
const BUILT_IN_WRAPPERS = [
|
|
45
41
|
...DEFAULT_TRANSPARENT_WRAPPERS,
|
|
46
42
|
BUILTIN_WRAPPER,
|
|
@@ -78,12 +74,11 @@ export const noDynamicInvocation = createRule({
|
|
|
78
74
|
type: 'object',
|
|
79
75
|
properties: {
|
|
80
76
|
allowInFunctions: NAME_LIST_SCHEMA,
|
|
81
|
-
allowWrappers: NAME_LIST_SCHEMA,
|
|
82
77
|
},
|
|
83
78
|
additionalProperties: false,
|
|
84
79
|
},
|
|
85
80
|
],
|
|
86
|
-
defaultOptions: [{ allowInFunctions: []
|
|
81
|
+
defaultOptions: [{ allowInFunctions: [] }],
|
|
87
82
|
messages: {
|
|
88
83
|
dynamicInvocation: 'The command this runs is not known until run time, so the script cannot be statically reviewed. Name the command with a literal word instead.',
|
|
89
84
|
embeddedCommand: 'This runs shell code embedded in a string, which cannot be statically reviewed. Write the commands directly instead.',
|
|
@@ -91,13 +86,10 @@ export const noDynamicInvocation = createRule({
|
|
|
91
86
|
},
|
|
92
87
|
create(context) {
|
|
93
88
|
const { sourceCode } = context;
|
|
94
|
-
//
|
|
95
|
-
const [{ allowInFunctions = []
|
|
89
|
+
// The options object exists by default; its dispatcher list may still be omitted.
|
|
90
|
+
const [{ allowInFunctions = [] }] = context.options;
|
|
96
91
|
const dispatchers = new Set(allowInFunctions);
|
|
97
|
-
const transparentWrappers =
|
|
98
|
-
...BUILT_IN_WRAPPERS,
|
|
99
|
-
...allowWrappers.map((name) => ({ names: [name] })),
|
|
100
|
-
];
|
|
92
|
+
const transparentWrappers = resolveTransparentWrappers(context, BUILT_IN_WRAPPERS);
|
|
101
93
|
return {
|
|
102
94
|
File() {
|
|
103
95
|
for (const site of sourceCode.getCommandSites()) {
|
package/dist/rules/no-eval.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* suggestion, because no mechanical rewrite of `eval` preserves behavior.
|
|
26
26
|
*/
|
|
27
27
|
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
28
|
+
import { resolveTransparentWrappers } from './resolve-transparent-wrappers.js';
|
|
28
29
|
import { createRule } from './create-rule.js';
|
|
29
30
|
import { BUILTIN_WRAPPER } from './transparent-wrappers.js';
|
|
30
31
|
/** The registry name of this rule (`sh/no-eval` in a user's config). */
|
|
@@ -60,10 +61,11 @@ export const noEval = createRule({
|
|
|
60
61
|
},
|
|
61
62
|
create(context) {
|
|
62
63
|
const { sourceCode } = context;
|
|
64
|
+
const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
|
|
63
65
|
return {
|
|
64
66
|
CallExpr(node) {
|
|
65
67
|
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
66
|
-
transparentWrappers
|
|
68
|
+
transparentWrappers,
|
|
67
69
|
});
|
|
68
70
|
if (!invoked?.effective.static) {
|
|
69
71
|
return;
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* Real shells give a plain assignment the status of a substitution in any of
|
|
16
16
|
* those places, so the declaration hides it from all of them. A process
|
|
17
17
|
* substitution (`<(…)`) is not looked into: it expands to a path. The builtin
|
|
18
|
-
* is also read behind the
|
|
18
|
+
* is also read behind the bare in-process wrappers `builtin` and `command`;
|
|
19
|
+
* paths to external executables are not declaration wrappers. One
|
|
19
20
|
* report is made per operand, on the operand. An array value
|
|
20
21
|
* (`local -a a=($(cmd))`) is not reported: shellcheck 0.11.0 does not report
|
|
21
22
|
* it either, and a status-bearing scalar is the construct the rule exists for.
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
* split into the declaration, the assignment, then `readonly <names>`.
|
|
85
86
|
*/
|
|
86
87
|
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
88
|
+
import { resolveTransparentWrappers } from './resolve-transparent-wrappers.js';
|
|
87
89
|
import { childNodes } from '../language/syntax-view.js';
|
|
88
90
|
import { lineBreakOf } from '../language/line-breaks.js';
|
|
89
91
|
import { createRule } from './create-rule.js';
|
|
@@ -218,10 +220,13 @@ function collectFunctionNames(node, into) {
|
|
|
218
220
|
* earlier, so such code can see a different variable afterwards. A function
|
|
219
221
|
* defined in another file cannot be seen and is not detected.
|
|
220
222
|
*/
|
|
221
|
-
function runsUnseenCode(node, functionNames, sourceCode) {
|
|
223
|
+
function runsUnseenCode(node, functionNames, sourceCode, transparentWrappers) {
|
|
222
224
|
if (node.type === 'CallExpr' && node.args.length > 0) {
|
|
225
|
+
// This check names the launched program; opaque child-interpreter text
|
|
226
|
+
// does not turn its program identity into dynamic lookup.
|
|
223
227
|
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
224
|
-
transparentWrappers
|
|
228
|
+
transparentWrappers,
|
|
229
|
+
embeddedCommandInterpreters: [],
|
|
225
230
|
});
|
|
226
231
|
if (invoked?.effective.static !== true)
|
|
227
232
|
return true;
|
|
@@ -229,7 +234,7 @@ function runsUnseenCode(node, functionNames, sourceCode) {
|
|
|
229
234
|
if (CODE_RUNNING_BUILTINS.has(name) || functionNames.has(name))
|
|
230
235
|
return true;
|
|
231
236
|
}
|
|
232
|
-
return childNodes(node).some((child) => runsUnseenCode(child, functionNames, sourceCode));
|
|
237
|
+
return childNodes(node).some((child) => runsUnseenCode(child, functionNames, sourceCode, transparentWrappers));
|
|
233
238
|
}
|
|
234
239
|
/**
|
|
235
240
|
* The state of `errexit` before each top-level statement of `file`, from the
|
|
@@ -446,7 +451,7 @@ function withoutReadonly(flag) {
|
|
|
446
451
|
* The replacement text for a standalone declaration, or `null` where no
|
|
447
452
|
* equivalent split exists. `separator` joins the statements.
|
|
448
453
|
*/
|
|
449
|
-
function splitText(declaration, scoped, separator, functionNames, sourceCode) {
|
|
454
|
+
function splitText(declaration, scoped, separator, functionNames, sourceCode, transparentWrappers) {
|
|
450
455
|
const { variant, flags, operands, flagsKnown } = declaration;
|
|
451
456
|
if (!flagsKnown || operands.some((operand) => operand.unsplittable))
|
|
452
457
|
return null;
|
|
@@ -489,7 +494,7 @@ function splitText(declaration, scoped, separator, functionNames, sourceCode) {
|
|
|
489
494
|
// operand declares nothing that code could observe differently.
|
|
490
495
|
const mayObserveDeclaration = scoped || operands.length > 1 || flags.length > 0;
|
|
491
496
|
if (mayObserveDeclaration &&
|
|
492
|
-
operands.some((operand) => operand.values.some((value) => runsUnseenCode(value, functionNames, sourceCode)))) {
|
|
497
|
+
operands.some((operand) => operand.values.some((value) => runsUnseenCode(value, functionNames, sourceCode, transparentWrappers)))) {
|
|
493
498
|
return null;
|
|
494
499
|
}
|
|
495
500
|
const makesReadonly = isReadonlyBuiltin || flags.some(isReadonlyFlag);
|
|
@@ -567,6 +572,7 @@ export const noMaskedExitStatus = createRule({
|
|
|
567
572
|
create(context) {
|
|
568
573
|
const [options] = context.options;
|
|
569
574
|
const { sourceCode } = context;
|
|
575
|
+
const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
|
|
570
576
|
const checked = new Set(options.builtins ?? ALL_BUILTINS);
|
|
571
577
|
const parentOf = (node) => sourceCode.getParent(node);
|
|
572
578
|
let errexitBefore = new Map();
|
|
@@ -613,7 +619,7 @@ export const noMaskedExitStatus = createRule({
|
|
|
613
619
|
const indent = text.slice(lineStart + 1, start);
|
|
614
620
|
const separator = /^[ \t]*$/u.test(indent) ? lineBreakOf(text) + indent : '; ';
|
|
615
621
|
const scoped = SCOPED_BUILTINS.has(builtin) && inFunction;
|
|
616
|
-
suggestion = splitText(declaration, scoped, separator, functionNames, sourceCode);
|
|
622
|
+
suggestion = splitText(declaration, scoped, separator, functionNames, sourceCode, transparentWrappers);
|
|
617
623
|
}
|
|
618
624
|
for (const operand of substituted) {
|
|
619
625
|
context.report({
|
|
@@ -645,7 +651,8 @@ export const noMaskedExitStatus = createRule({
|
|
|
645
651
|
},
|
|
646
652
|
CallExpr(node) {
|
|
647
653
|
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
648
|
-
transparentWrappers
|
|
654
|
+
transparentWrappers,
|
|
655
|
+
embeddedCommandInterpreters: [],
|
|
649
656
|
});
|
|
650
657
|
if (invoked?.effective.static !== true)
|
|
651
658
|
return;
|
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export interface NoSecretEnvReadsOptions {
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Case-sensitive Unicode regex sources tested against each expanded
|
|
9
|
+
* variable's name. Replaces
|
|
9
10
|
* {@link DEFAULT_PATTERNS} entirely.
|
|
10
11
|
*/
|
|
11
12
|
readonly patterns?: readonly string[];
|
|
12
13
|
/**
|
|
13
|
-
* Command names whose arguments may read a secret-shaped variable. A
|
|
14
|
-
*
|
|
14
|
+
* Command names whose own arguments may read a secret-shaped variable. Wrapper-owned operands are still checked normally. A command
|
|
15
|
+
* run by path matches by basename.
|
|
15
16
|
*/
|
|
16
17
|
readonly allowedSinks?: readonly string[];
|
|
17
18
|
}
|
|
@@ -22,12 +22,14 @@
|
|
|
22
22
|
* command that runs (`TOKEN=$GITHUB_TOKEN tool`) is the recommended
|
|
23
23
|
* pattern and is clean. A read nested deeper (a command substitution in
|
|
24
24
|
* that value) belongs to the inner command and is judged there.
|
|
25
|
-
* - A read in
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* - A read in an argument after the final allow-listed sink command is clean.
|
|
26
|
+
* Wrapper-owned flags and operands receive no sink exemption. The nearest
|
|
27
|
+
* enclosing `CallExpr` supplies argv ownership, and its resolved final
|
|
28
|
+
* command index separates wrapper operands from the sink's arguments.
|
|
28
29
|
* - Matching is a name pattern, not secret detection.
|
|
29
30
|
*/
|
|
30
31
|
import { AlternateUnset, AlternateUnsetOrNull } from 'sh-ast/analyze';
|
|
32
|
+
import { resolveTransparentWrappers } from './resolve-transparent-wrappers.js';
|
|
31
33
|
import { createRule } from './create-rule.js';
|
|
32
34
|
import { compileUserPattern } from './user-pattern.js';
|
|
33
35
|
/**
|
|
@@ -71,7 +73,9 @@ export const noSecretEnvReads = createRule({
|
|
|
71
73
|
properties: {
|
|
72
74
|
patterns: {
|
|
73
75
|
type: 'array',
|
|
74
|
-
|
|
76
|
+
// The shared compiler validates Unicode syntax and identifies
|
|
77
|
+
// the option index and source; flagless schema checks cannot.
|
|
78
|
+
items: { type: 'string' },
|
|
75
79
|
minItems: 1,
|
|
76
80
|
},
|
|
77
81
|
allowedSinks: {
|
|
@@ -89,33 +93,52 @@ export const noSecretEnvReads = createRule({
|
|
|
89
93
|
},
|
|
90
94
|
create(context) {
|
|
91
95
|
const options = context.options[0];
|
|
92
|
-
const patterns = (options?.patterns ?? DEFAULT_PATTERNS).map((source, index) => compileUserPattern('sh/no-secret-env-reads', `patterns[${String(index)}]`, source, ''));
|
|
96
|
+
const patterns = (options?.patterns ?? DEFAULT_PATTERNS).map((source, index) => compileUserPattern('sh/no-secret-env-reads', `patterns[${String(index)}]`, source, 'u'));
|
|
93
97
|
const allowedSinks = new Set(options?.allowedSinks ?? []);
|
|
94
98
|
const { sourceCode } = context;
|
|
99
|
+
const transparentWrappers = resolveTransparentWrappers(context);
|
|
95
100
|
const isSecretName = (name) => patterns.some((pattern) => pattern.test(name));
|
|
96
|
-
/** Whether
|
|
97
|
-
function isAllowedSink(callExpr) {
|
|
98
|
-
if (allowedSinks.size === 0)
|
|
101
|
+
/** Whether this direct argument word belongs to the final approved sink. */
|
|
102
|
+
function isAllowedSink(callExpr, argument) {
|
|
103
|
+
if (allowedSinks.size === 0 || argument.type !== 'Word') {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// Sink policy names the program receiving these arguments. An interpreter's
|
|
107
|
+
// command-string selector does not change that recipient; judging its code
|
|
108
|
+
// belongs to no-dynamic-invocation rather than this explicit allowance.
|
|
109
|
+
const invoked = sourceCode.resolveInvokedCommand(callExpr, {
|
|
110
|
+
transparentWrappers,
|
|
111
|
+
embeddedCommandInterpreters: [],
|
|
112
|
+
});
|
|
113
|
+
if (!invoked?.effective.static) {
|
|
99
114
|
return false;
|
|
100
|
-
|
|
101
|
-
|
|
115
|
+
}
|
|
116
|
+
const effectiveIndex = invoked.chainIndices.at(-1);
|
|
117
|
+
if (effectiveIndex === undefined || callExpr.args.indexOf(argument) <= effectiveIndex) {
|
|
102
118
|
return false;
|
|
119
|
+
}
|
|
120
|
+
const { effective } = invoked;
|
|
103
121
|
return allowedSinks.has(effective.text) || allowedSinks.has(basename(effective.text));
|
|
104
122
|
}
|
|
105
123
|
/**
|
|
106
|
-
* Whether the read at `node` is permitted: it sits in
|
|
107
|
-
* allowed sink, or
|
|
124
|
+
* Whether the read at `node` is permitted: it sits in an actual argument of
|
|
125
|
+
* the final allowed sink, or in an assignment prefix of a command that runs.
|
|
108
126
|
* The nearest enclosing `Assign` or `CallExpr` decides, so a read inside a
|
|
109
127
|
* command substitution is judged by the inner command, not the outer one.
|
|
110
128
|
*/
|
|
111
129
|
function isPermittedRead(node) {
|
|
130
|
+
// Keep the direct child by identity: argument ownership needs no range
|
|
131
|
+
// arithmetic and nested commands retain their own nearest call boundary.
|
|
132
|
+
let child = node;
|
|
112
133
|
for (let up = sourceCode.getParent(node); up !== undefined; up = sourceCode.getParent(up)) {
|
|
113
|
-
if (up.type === 'CallExpr')
|
|
114
|
-
return isAllowedSink(up);
|
|
134
|
+
if (up.type === 'CallExpr') {
|
|
135
|
+
return isAllowedSink(up, child);
|
|
136
|
+
}
|
|
115
137
|
if (up.type === 'Assign') {
|
|
116
138
|
const owner = sourceCode.getParent(up);
|
|
117
139
|
return owner?.type === 'CallExpr' && owner.args.length > 0;
|
|
118
140
|
}
|
|
141
|
+
child = up;
|
|
119
142
|
}
|
|
120
143
|
return false;
|
|
121
144
|
}
|
|
@@ -7,7 +7,8 @@ import type { ShellNode } from '../language/syntax-view.js';
|
|
|
7
7
|
export interface NoSecretFlagsOptions {
|
|
8
8
|
/**
|
|
9
9
|
* Regex sources tested against each flag's name (the word up to, not
|
|
10
|
-
* including, its first `=`)
|
|
10
|
+
* including, its first `=`), with Unicode case-insensitive matching.
|
|
11
|
+
* Replaces {@link DEFAULT_PATTERNS} entirely.
|
|
11
12
|
*/
|
|
12
13
|
readonly patterns?: readonly string[];
|
|
13
14
|
}
|
|
@@ -55,14 +55,11 @@ const DEFAULT_PATTERNS = [
|
|
|
55
55
|
'^(?!.*-(?:stdin|file)$)--?(?:access-|auth-?)?(?:password|passwd|token|secret|api-?key|auth(?:key|orization)?)(?![a-z])',
|
|
56
56
|
];
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* a few sources valid there are syntax errors under `u`. Adding `i` never
|
|
62
|
-
* turns a valid source into an invalid one, so a pattern that passed
|
|
63
|
-
* validation always compiles here.
|
|
58
|
+
* Unicode matching that ignores case for both default and custom sources.
|
|
59
|
+
* Keep the original flag name and pattern intact: lower-casing the subject
|
|
60
|
+
* would make a custom pattern containing capitals silently stop matching.
|
|
64
61
|
*/
|
|
65
|
-
const PATTERN_FLAGS = '
|
|
62
|
+
const PATTERN_FLAGS = 'iu';
|
|
66
63
|
/**
|
|
67
64
|
* Splits a static argument word into the flag name the patterns are tested
|
|
68
65
|
* against and the value written in the same word, if any. `--token=abc` is
|
|
@@ -100,7 +97,9 @@ export const noSecretFlags = createRule({
|
|
|
100
97
|
properties: {
|
|
101
98
|
patterns: {
|
|
102
99
|
type: 'array',
|
|
103
|
-
|
|
100
|
+
// Syntax is checked with the actual flags by compileUserPattern,
|
|
101
|
+
// which also names the option index and source in errors.
|
|
102
|
+
items: { type: 'string' },
|
|
104
103
|
minItems: 1,
|
|
105
104
|
},
|
|
106
105
|
},
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
* statement is the document body, not shell code).
|
|
49
49
|
*/
|
|
50
50
|
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
51
|
+
import { resolveTransparentWrappers } from './resolve-transparent-wrappers.js';
|
|
51
52
|
import { createRule } from './create-rule.js';
|
|
52
53
|
import { prologueEnabledOptions, setStatementChanges } from './require-strict-mode.js';
|
|
53
54
|
import { BUILTIN_WRAPPER } from './transparent-wrappers.js';
|
|
@@ -202,6 +203,7 @@ export const noUncheckedCd = createRule({
|
|
|
202
203
|
create(context) {
|
|
203
204
|
const [options] = context.options;
|
|
204
205
|
const { sourceCode } = context;
|
|
206
|
+
const transparentWrappers = resolveTransparentWrappers(context, TRANSPARENT_WRAPPERS);
|
|
205
207
|
let errexitInForce = false;
|
|
206
208
|
return {
|
|
207
209
|
File(node) {
|
|
@@ -213,7 +215,7 @@ export const noUncheckedCd = createRule({
|
|
|
213
215
|
if (errexitInForce)
|
|
214
216
|
return;
|
|
215
217
|
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
216
|
-
transparentWrappers
|
|
218
|
+
transparentWrappers,
|
|
217
219
|
});
|
|
218
220
|
if (invoked?.effective.static !== true || !DIRECTORY_COMMANDS.has(invoked.effective.text)) {
|
|
219
221
|
return;
|
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rule's single options object.
|
|
3
|
-
*
|
|
4
|
-
* @internal
|
|
5
|
-
*/
|
|
6
|
-
export interface NoUndeclaredCommandsOptions {
|
|
7
|
-
/**
|
|
8
|
-
* Names of project-specific transparent wrappers (`with-retry`), added to
|
|
9
|
-
* the default wrapper table (see `harvestWrapperTable`). The word after a listed
|
|
10
|
-
* wrapper is the command it runs, so `with-retry curl` uses `curl` as well
|
|
11
|
-
* as `with-retry`.
|
|
12
|
-
*/
|
|
13
|
-
readonly transparentWrappers?: readonly string[];
|
|
14
|
-
}
|
|
15
1
|
/**
|
|
16
2
|
* The `sh/no-undeclared-commands` rule module.
|
|
17
3
|
*
|
|
18
4
|
* @internal
|
|
19
5
|
*/
|
|
20
|
-
export declare const noUndeclaredCommands: import("./define-shell-rule.js").ShellRule<[
|
|
6
|
+
export declare const noUndeclaredCommands: import("./define-shell-rule.js").ShellRule<[], "undeclaredCommand">;
|
|
@@ -33,7 +33,7 @@ import { harvestWrapperTable } from '../command-harvest/wrapper-table.js';
|
|
|
33
33
|
import { createRule } from './create-rule.js';
|
|
34
34
|
import { readDeclaredCommands, toLintedFile, } from '../declaration-conventions/read-declared-commands.js';
|
|
35
35
|
import { resolveDeclarationConvention } from './resolve-declaration-convention.js';
|
|
36
|
-
import {
|
|
36
|
+
import { resolveTransparentWrappers } from './resolve-transparent-wrappers.js';
|
|
37
37
|
/** The registry name of this rule (`sh/no-undeclared-commands` in a user's config). */
|
|
38
38
|
const RULE_NAME = 'no-undeclared-commands';
|
|
39
39
|
/** The report's second sentence when the convention offers no hint for a command. */
|
|
@@ -67,19 +67,7 @@ export const noUndeclaredCommands = createRule({
|
|
|
67
67
|
docs: {
|
|
68
68
|
description: 'Require every external command a script uses to be declared by the configured declaration convention',
|
|
69
69
|
},
|
|
70
|
-
schema: [
|
|
71
|
-
{
|
|
72
|
-
type: 'object',
|
|
73
|
-
properties: {
|
|
74
|
-
transparentWrappers: {
|
|
75
|
-
type: 'array',
|
|
76
|
-
items: { type: 'string', minLength: 1 },
|
|
77
|
-
uniqueItems: true,
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
additionalProperties: false,
|
|
81
|
-
},
|
|
82
|
-
],
|
|
70
|
+
schema: [],
|
|
83
71
|
messages: {
|
|
84
72
|
undeclaredCommand: "'{{command}}' is used but not declared. {{howToDeclare}}",
|
|
85
73
|
},
|
|
@@ -90,7 +78,7 @@ export const noUndeclaredCommands = createRule({
|
|
|
90
78
|
return {};
|
|
91
79
|
}
|
|
92
80
|
const { sourceCode } = context;
|
|
93
|
-
const transparentWrappers =
|
|
81
|
+
const transparentWrappers = resolveTransparentWrappers(context, harvestWrapperTable(sourceCode.dialect));
|
|
94
82
|
return {
|
|
95
83
|
File() {
|
|
96
84
|
// Only names matter here, so every kind of declaration counts the same.
|
|
@@ -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">;
|