@eslint/shell 0.0.0-stage → 0.1.2
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/LICENSE +201 -0
- package/README.md +167 -2
- package/dist/index.d.ts +79 -0
- package/dist/index.js +74 -0
- package/dist/languages/shell-language.d.ts +36 -0
- package/dist/languages/shell-language.js +72 -0
- package/dist/languages/shell-source-code.d.ts +55 -0
- package/dist/languages/shell-source-code.js +184 -0
- package/dist/parser/parse.d.ts +32 -0
- package/dist/parser/parse.js +758 -0
- package/dist/rules/no-backticks.d.ts +8 -0
- package/dist/rules/no-backticks.js +46 -0
- package/dist/rules/no-expansions-in-single-quotes.d.ts +8 -0
- package/dist/rules/no-expansions-in-single-quotes.js +42 -0
- package/dist/rules/no-ls-iteration.d.ts +8 -0
- package/dist/rules/no-ls-iteration.js +42 -0
- package/dist/rules/no-unquoted-expansions.d.ts +9 -0
- package/dist/rules/no-unquoted-expansions.js +91 -0
- package/dist/rules/no-unused-vars.d.ts +11 -0
- package/dist/rules/no-unused-vars.js +382 -0
- package/dist/rules/no-useless-cat.d.ts +8 -0
- package/dist/rules/no-useless-cat.js +76 -0
- package/dist/rules/no-useless-echo.d.ts +8 -0
- package/dist/rules/no-useless-echo.js +42 -0
- package/dist/rules/no-variables-in-printf-format.d.ts +8 -0
- package/dist/rules/no-variables-in-printf-format.js +60 -0
- package/dist/rules/require-cd-guard.d.ts +8 -0
- package/dist/rules/require-cd-guard.js +74 -0
- package/dist/rules/require-read-r.d.ts +8 -0
- package/dist/rules/require-read-r.js +83 -0
- package/dist/rules/utils.d.ts +35 -0
- package/dist/rules/utils.js +100 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.js +7 -0
- package/dist/visitor-keys.d.ts +5 -0
- package/dist/visitor-keys.js +59 -0
- package/package.json +75 -4
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow legacy backtick command substitution.
|
|
3
|
+
*/
|
|
4
|
+
//------------------------------------------------------------------------------
|
|
5
|
+
// Rule Definition
|
|
6
|
+
//------------------------------------------------------------------------------
|
|
7
|
+
const rule = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "suggestion",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Disallow legacy backtick command substitution in favor of `$(...)`",
|
|
12
|
+
recommended: true,
|
|
13
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/no-backticks.md",
|
|
14
|
+
},
|
|
15
|
+
fixable: "code",
|
|
16
|
+
schema: [],
|
|
17
|
+
messages: {
|
|
18
|
+
useDollarParen: "Use $(...) notation instead of legacy backticks.",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
const { sourceCode } = context;
|
|
23
|
+
return {
|
|
24
|
+
CommandSubstitution(node) {
|
|
25
|
+
if (!node.backquotes) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const [start, end] = sourceCode.getRange(node);
|
|
29
|
+
const inner = sourceCode.text.slice(start + 1, end - 1);
|
|
30
|
+
context.report({
|
|
31
|
+
node,
|
|
32
|
+
messageId: "useDollarParen",
|
|
33
|
+
// Escapes behave differently inside backticks, so only
|
|
34
|
+
// fix substitutions without backslashes or nesting.
|
|
35
|
+
fix: /[\\`]/u.test(inner)
|
|
36
|
+
? undefined
|
|
37
|
+
: fixer => [
|
|
38
|
+
fixer.replaceTextRange([start, start + 1], "$("),
|
|
39
|
+
fixer.replaceTextRange([end - 1, end], ")"),
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
export default rule;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to flag expansion-like text inside single quotes.
|
|
3
|
+
*/
|
|
4
|
+
//------------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
//------------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* Matches text that looks like an intended expansion: `$name`, `${...}`,
|
|
9
|
+
* `$(...)`, or backticks. Positional-style references such as `$1` are
|
|
10
|
+
* excluded because they are common in awk/sed programs.
|
|
11
|
+
*/
|
|
12
|
+
const EXPANSION_LIKE = /\$[A-Za-z_{(]|`[^`]+`/u;
|
|
13
|
+
//------------------------------------------------------------------------------
|
|
14
|
+
// Rule Definition
|
|
15
|
+
//------------------------------------------------------------------------------
|
|
16
|
+
const rule = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: "suggestion",
|
|
19
|
+
docs: {
|
|
20
|
+
description: "Disallow expansion-like syntax inside single quotes, where it is not expanded",
|
|
21
|
+
recommended: true,
|
|
22
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/no-expansions-in-single-quotes.md",
|
|
23
|
+
},
|
|
24
|
+
schema: [],
|
|
25
|
+
messages: {
|
|
26
|
+
expansionInSingleQuotes: "Expressions don't expand in single quotes; use double quotes for that.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
create(context) {
|
|
30
|
+
return {
|
|
31
|
+
SingleQuotedString(node) {
|
|
32
|
+
if (!node.dollar && EXPANSION_LIKE.test(node.value)) {
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
messageId: "expansionInSingleQuotes",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
export default rule;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow iterating over `ls` output.
|
|
3
|
+
*/
|
|
4
|
+
//------------------------------------------------------------------------------
|
|
5
|
+
// Imports
|
|
6
|
+
//------------------------------------------------------------------------------
|
|
7
|
+
import { startsWithCommand } from "./utils.js";
|
|
8
|
+
//------------------------------------------------------------------------------
|
|
9
|
+
// Rule Definition
|
|
10
|
+
//------------------------------------------------------------------------------
|
|
11
|
+
const rule = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "Disallow iterating over `ls` output, which breaks on special characters",
|
|
16
|
+
recommended: true,
|
|
17
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/no-ls-iteration.md",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
lsIteration: "Iterating over ls output is fragile. Use globs (e.g. *) instead.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
return {
|
|
26
|
+
ForStatement(node) {
|
|
27
|
+
for (const word of node.words) {
|
|
28
|
+
for (const part of word.parts) {
|
|
29
|
+
if (part.type === "CommandSubstitution" &&
|
|
30
|
+
startsWithCommand(part.body, "ls")) {
|
|
31
|
+
context.report({
|
|
32
|
+
node: part,
|
|
33
|
+
messageId: "lsIteration",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
export default rule;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to require quoting expansions that undergo word
|
|
3
|
+
* splitting and globbing.
|
|
4
|
+
*/
|
|
5
|
+
import type { ShellRuleDefinition } from "../types.js";
|
|
6
|
+
declare const rule: ShellRuleDefinition<{
|
|
7
|
+
MessageIds: "unquotedParameterExpansion" | "unquotedCommandSubstitution";
|
|
8
|
+
}>;
|
|
9
|
+
export default rule;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to require quoting expansions that undergo word
|
|
3
|
+
* splitting and globbing.
|
|
4
|
+
*/
|
|
5
|
+
import { startsWithCommand } from "./utils.js";
|
|
6
|
+
//------------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
//------------------------------------------------------------------------------
|
|
9
|
+
/**
|
|
10
|
+
* Parameters that always expand to values that cannot split, so quoting is
|
|
11
|
+
* unnecessary.
|
|
12
|
+
*/
|
|
13
|
+
const SAFE_PARAMETERS = new Set(["?", "$", "!", "#", "-"]);
|
|
14
|
+
/** Heredoc-style redirect operators whose targets never word-split. */
|
|
15
|
+
const NON_SPLITTING_REDIRECTS = new Set(["<<", "<<-", "<<<"]);
|
|
16
|
+
//------------------------------------------------------------------------------
|
|
17
|
+
// Rule Definition
|
|
18
|
+
//------------------------------------------------------------------------------
|
|
19
|
+
const rule = {
|
|
20
|
+
meta: {
|
|
21
|
+
type: "problem",
|
|
22
|
+
docs: {
|
|
23
|
+
description: "Require quoting parameter expansions and command substitutions that are subject to word splitting",
|
|
24
|
+
recommended: true,
|
|
25
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/no-unquoted-expansions.md",
|
|
26
|
+
},
|
|
27
|
+
fixable: "code",
|
|
28
|
+
schema: [],
|
|
29
|
+
messages: {
|
|
30
|
+
unquotedParameterExpansion: 'Double quote "{{expansion}}" to prevent word splitting and globbing.',
|
|
31
|
+
unquotedCommandSubstitution: "Quote this command substitution to prevent word splitting.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
create(context) {
|
|
35
|
+
const { sourceCode } = context;
|
|
36
|
+
function checkWord(word, isForWord = false) {
|
|
37
|
+
for (const part of word.parts) {
|
|
38
|
+
if (part.type !== "ParameterExpansion" &&
|
|
39
|
+
part.type !== "CommandSubstitution") {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (part.type === "ParameterExpansion") {
|
|
43
|
+
if (part.lengthOf || SAFE_PARAMETERS.has(part.name)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Only offer a fix when the expansion is the entire word.
|
|
48
|
+
const isWholeWord = word.parts.length === 1 &&
|
|
49
|
+
word.start === part.start &&
|
|
50
|
+
word.end === part.end;
|
|
51
|
+
const isLsIteration = isForWord &&
|
|
52
|
+
part.type === "CommandSubstitution" &&
|
|
53
|
+
startsWithCommand(part.body, "ls");
|
|
54
|
+
context.report({
|
|
55
|
+
node: part,
|
|
56
|
+
messageId: part.type === "ParameterExpansion"
|
|
57
|
+
? "unquotedParameterExpansion"
|
|
58
|
+
: "unquotedCommandSubstitution",
|
|
59
|
+
data: part.type === "ParameterExpansion"
|
|
60
|
+
? { expansion: sourceCode.getText(part) }
|
|
61
|
+
: {},
|
|
62
|
+
fix: isWholeWord && !isLsIteration
|
|
63
|
+
? fixer => fixer.replaceText(word, `"${sourceCode.getText(word)}"`)
|
|
64
|
+
: undefined,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
Command(node) {
|
|
70
|
+
if (node.name) {
|
|
71
|
+
checkWord(node.name);
|
|
72
|
+
}
|
|
73
|
+
for (const argument of node.arguments) {
|
|
74
|
+
checkWord(argument);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
ForStatement(node) {
|
|
78
|
+
for (const word of node.words) {
|
|
79
|
+
checkWord(word, true);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
Redirect(node) {
|
|
83
|
+
if (node.target &&
|
|
84
|
+
!NON_SPLITTING_REDIRECTS.has(node.operator)) {
|
|
85
|
+
checkWord(node.target);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
export default rule;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to flag variables that are assigned but never used.
|
|
3
|
+
*/
|
|
4
|
+
import type { ShellRuleDefinition } from "../types.js";
|
|
5
|
+
declare const rule: ShellRuleDefinition<{
|
|
6
|
+
MessageIds: "unusedVariable";
|
|
7
|
+
RuleOptions: [{
|
|
8
|
+
allowed?: string[];
|
|
9
|
+
}?];
|
|
10
|
+
}>;
|
|
11
|
+
export default rule;
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to flag variables that are assigned but never used.
|
|
3
|
+
*/
|
|
4
|
+
//------------------------------------------------------------------------------
|
|
5
|
+
// Imports
|
|
6
|
+
//------------------------------------------------------------------------------
|
|
7
|
+
import { getCommandName, getStaticText } from "./utils.js";
|
|
8
|
+
//------------------------------------------------------------------------------
|
|
9
|
+
// Helpers
|
|
10
|
+
//------------------------------------------------------------------------------
|
|
11
|
+
/** Variables that are meaningful to the shell or to common external tools. */
|
|
12
|
+
const SPECIAL_VARIABLES = new Set([
|
|
13
|
+
"BASH_ENV",
|
|
14
|
+
"CDPATH",
|
|
15
|
+
"EDITOR",
|
|
16
|
+
"ENV",
|
|
17
|
+
"GLOBIGNORE",
|
|
18
|
+
"HISTCONTROL",
|
|
19
|
+
"HISTFILE",
|
|
20
|
+
"HISTFILESIZE",
|
|
21
|
+
"HISTSIZE",
|
|
22
|
+
"HOME",
|
|
23
|
+
"IFS",
|
|
24
|
+
"LANG",
|
|
25
|
+
"LD_LIBRARY_PATH",
|
|
26
|
+
"OPTARG",
|
|
27
|
+
"OPTERR",
|
|
28
|
+
"OPTIND",
|
|
29
|
+
"PAGER",
|
|
30
|
+
"PATH",
|
|
31
|
+
"PROMPT_COMMAND",
|
|
32
|
+
"PS1",
|
|
33
|
+
"PS2",
|
|
34
|
+
"PS3",
|
|
35
|
+
"PS4",
|
|
36
|
+
"REPLY",
|
|
37
|
+
"TERM",
|
|
38
|
+
"TMOUT",
|
|
39
|
+
"TZ",
|
|
40
|
+
"VISUAL",
|
|
41
|
+
]);
|
|
42
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*/u;
|
|
43
|
+
/** `read` flags that consume the following word as their argument. */
|
|
44
|
+
const READ_FLAGS_WITH_ARGUMENT = /^-[a-zA-Z]*[dinNptu]$/u;
|
|
45
|
+
/** `mapfile`/`readarray` flags that consume the following word. */
|
|
46
|
+
const MAPFILE_FLAGS_WITH_ARGUMENT = /^-[a-zA-Z]*[cCdnOsu]$/u;
|
|
47
|
+
/** Test operators whose operands are evaluated arithmetically. */
|
|
48
|
+
const ARITHMETIC_TEST_OPERATORS = new Set([
|
|
49
|
+
"-eq",
|
|
50
|
+
"-ne",
|
|
51
|
+
"-lt",
|
|
52
|
+
"-le",
|
|
53
|
+
"-gt",
|
|
54
|
+
"-ge",
|
|
55
|
+
]);
|
|
56
|
+
/** Arithmetic operators that assign to their left operand. */
|
|
57
|
+
const ARITHMETIC_ASSIGNMENT = /^(?:=|\+=|-=|\*=|\/=|%=|<<=|>>=|&=|\^=|\|=)$/u;
|
|
58
|
+
//------------------------------------------------------------------------------
|
|
59
|
+
// Rule Definition
|
|
60
|
+
//------------------------------------------------------------------------------
|
|
61
|
+
const rule = {
|
|
62
|
+
meta: {
|
|
63
|
+
type: "problem",
|
|
64
|
+
docs: {
|
|
65
|
+
description: "Disallow variables that are assigned but never used",
|
|
66
|
+
recommended: true,
|
|
67
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/no-unused-vars.md",
|
|
68
|
+
},
|
|
69
|
+
schema: [
|
|
70
|
+
{
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
allowed: {
|
|
74
|
+
type: "array",
|
|
75
|
+
items: { type: "string" },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
additionalProperties: false,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
defaultOptions: [{}],
|
|
82
|
+
messages: {
|
|
83
|
+
unusedVariable: "Variable '{{name}}' is assigned but never used.",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
create(context) {
|
|
87
|
+
const [{ allowed = [] } = {}] = context.options;
|
|
88
|
+
const allowedNames = new Set(allowed);
|
|
89
|
+
const writes = new Map();
|
|
90
|
+
const reads = new Set();
|
|
91
|
+
// Set when the file uses constructs that can reference variables
|
|
92
|
+
// dynamically (eval, source), making usage analysis unreliable.
|
|
93
|
+
let unsafe = false;
|
|
94
|
+
function addWrite(name, node) {
|
|
95
|
+
if (!writes.has(name)) {
|
|
96
|
+
writes.set(name, node);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function addReadableIdentifier(word) {
|
|
100
|
+
const text = getStaticText(word);
|
|
101
|
+
const match = text === null ? null : IDENTIFIER.exec(text);
|
|
102
|
+
if (match) {
|
|
103
|
+
reads.add(match[0]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Records reads/writes for bare identifiers inside arithmetic
|
|
108
|
+
* expressions, where `x` references the variable without `$`.
|
|
109
|
+
*/
|
|
110
|
+
function scanArithmetic(expression, isWrite) {
|
|
111
|
+
switch (expression.type) {
|
|
112
|
+
case "Word": {
|
|
113
|
+
const text = getStaticText(expression);
|
|
114
|
+
const match = text === null ? null : IDENTIFIER.exec(text);
|
|
115
|
+
if (match && match[0] === text) {
|
|
116
|
+
if (isWrite) {
|
|
117
|
+
const identifier = {
|
|
118
|
+
type: "Identifier",
|
|
119
|
+
start: expression.start,
|
|
120
|
+
end: expression.end,
|
|
121
|
+
name: text,
|
|
122
|
+
};
|
|
123
|
+
addWrite(text, identifier);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
reads.add(match[0]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "BinaryArithmetic":
|
|
132
|
+
if (ARITHMETIC_ASSIGNMENT.test(expression.operator)) {
|
|
133
|
+
scanArithmetic(expression.left, true);
|
|
134
|
+
// Compound assignments also read the variable.
|
|
135
|
+
if (expression.operator !== "=") {
|
|
136
|
+
scanArithmetic(expression.left, false);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
scanArithmetic(expression.left, false);
|
|
141
|
+
}
|
|
142
|
+
scanArithmetic(expression.right, false);
|
|
143
|
+
break;
|
|
144
|
+
case "UnaryArithmetic":
|
|
145
|
+
if (expression.operator === "++" ||
|
|
146
|
+
expression.operator === "--") {
|
|
147
|
+
scanArithmetic(expression.argument, true);
|
|
148
|
+
}
|
|
149
|
+
scanArithmetic(expression.argument, false);
|
|
150
|
+
break;
|
|
151
|
+
case "ParenthesizedArithmetic":
|
|
152
|
+
scanArithmetic(expression.expression, false);
|
|
153
|
+
break;
|
|
154
|
+
// no default
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Records reads for bare identifiers used as operands of arithmetic
|
|
159
|
+
* comparisons in `[[ ... ]]`, such as `[[ x -gt 0 ]]`.
|
|
160
|
+
*/
|
|
161
|
+
function scanTest(expression) {
|
|
162
|
+
switch (expression.type) {
|
|
163
|
+
case "BinaryTest":
|
|
164
|
+
if (ARITHMETIC_TEST_OPERATORS.has(expression.operator)) {
|
|
165
|
+
for (const operand of [
|
|
166
|
+
expression.left,
|
|
167
|
+
expression.right,
|
|
168
|
+
]) {
|
|
169
|
+
if (operand.type === "Word") {
|
|
170
|
+
scanArithmetic(operand, false);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
scanTest(expression.left);
|
|
176
|
+
scanTest(expression.right);
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case "UnaryTest":
|
|
180
|
+
scanTest(expression.argument);
|
|
181
|
+
break;
|
|
182
|
+
case "ParenthesizedTest":
|
|
183
|
+
scanTest(expression.expression);
|
|
184
|
+
break;
|
|
185
|
+
// no default
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
VariableAssignment(node) {
|
|
190
|
+
if (node.name) {
|
|
191
|
+
const parent = context.sourceCode.getParent(node);
|
|
192
|
+
// Environment prefixes such as `FOO=1 cmd` are passed to
|
|
193
|
+
// the command and therefore used.
|
|
194
|
+
if (parent?.type === "Command" && parent.name !== null) {
|
|
195
|
+
reads.add(node.name.name);
|
|
196
|
+
}
|
|
197
|
+
addWrite(node.name.name, node.name);
|
|
198
|
+
}
|
|
199
|
+
if (node.index) {
|
|
200
|
+
scanArithmetic(node.index, false);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
ArrayElement(node) {
|
|
204
|
+
if (node.index) {
|
|
205
|
+
scanArithmetic(node.index, false);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
ParameterExpansion(node) {
|
|
209
|
+
reads.add(node.name);
|
|
210
|
+
for (const expression of [
|
|
211
|
+
node.index,
|
|
212
|
+
node.sliceOffset,
|
|
213
|
+
node.sliceLength,
|
|
214
|
+
]) {
|
|
215
|
+
if (expression) {
|
|
216
|
+
scanArithmetic(expression, false);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
ForStatement(node) {
|
|
221
|
+
if (node.variable) {
|
|
222
|
+
addWrite(node.variable.name, node.variable);
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
TestCommand(node) {
|
|
226
|
+
scanTest(node.expression);
|
|
227
|
+
},
|
|
228
|
+
ArithmeticCommand(node) {
|
|
229
|
+
if (node.expression) {
|
|
230
|
+
scanArithmetic(node.expression, false);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
ArithmeticExpansion(node) {
|
|
234
|
+
if (node.expression) {
|
|
235
|
+
scanArithmetic(node.expression, false);
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
ArithmeticForStatement(node) {
|
|
239
|
+
for (const expression of [node.init, node.test, node.update]) {
|
|
240
|
+
if (expression) {
|
|
241
|
+
scanArithmetic(expression, false);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
LetCommand(node) {
|
|
246
|
+
for (const expression of node.expressions) {
|
|
247
|
+
scanArithmetic(expression, false);
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
DeclarationCommand(node) {
|
|
251
|
+
let exported = node.kind === "export";
|
|
252
|
+
let nameref = node.kind === "nameref";
|
|
253
|
+
let printed = false;
|
|
254
|
+
for (const argument of node.arguments) {
|
|
255
|
+
if (argument.type === "Word") {
|
|
256
|
+
const text = getStaticText(argument);
|
|
257
|
+
if (text !== null && text.startsWith("-")) {
|
|
258
|
+
if (text.includes("x")) {
|
|
259
|
+
exported = true;
|
|
260
|
+
}
|
|
261
|
+
// `export -n` removes the export attribute
|
|
262
|
+
// rather than creating a nameref.
|
|
263
|
+
if (text.includes("n")) {
|
|
264
|
+
if (node.kind === "export") {
|
|
265
|
+
exported = false;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
nameref = true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (text.includes("p")) {
|
|
272
|
+
printed = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (nameref) {
|
|
278
|
+
unsafe = true;
|
|
279
|
+
}
|
|
280
|
+
for (const argument of node.arguments) {
|
|
281
|
+
if (argument.type !== "VariableAssignment" ||
|
|
282
|
+
!argument.name) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
// Exported variables are visible to child processes, so
|
|
286
|
+
// consider them used. Naming an existing variable in
|
|
287
|
+
// `readonly x` or `declare -p x` also references it.
|
|
288
|
+
if (exported ||
|
|
289
|
+
(argument.value === null &&
|
|
290
|
+
argument.array === null &&
|
|
291
|
+
(node.kind === "readonly" || printed))) {
|
|
292
|
+
reads.add(argument.name.name);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
Command(node) {
|
|
297
|
+
const name = getCommandName(node);
|
|
298
|
+
switch (name) {
|
|
299
|
+
case "eval":
|
|
300
|
+
case "source":
|
|
301
|
+
case ".":
|
|
302
|
+
unsafe = true;
|
|
303
|
+
break;
|
|
304
|
+
case "unset":
|
|
305
|
+
case "export":
|
|
306
|
+
case "readonly":
|
|
307
|
+
for (const argument of node.arguments) {
|
|
308
|
+
addReadableIdentifier(argument);
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
case "getopts": {
|
|
312
|
+
const variable = node.arguments[1];
|
|
313
|
+
if (variable) {
|
|
314
|
+
const text = getStaticText(variable);
|
|
315
|
+
if (text !== null && IDENTIFIER.test(text)) {
|
|
316
|
+
addWrite(text, {
|
|
317
|
+
type: "Identifier",
|
|
318
|
+
start: variable.start,
|
|
319
|
+
end: variable.end,
|
|
320
|
+
name: text,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case "read":
|
|
327
|
+
case "mapfile":
|
|
328
|
+
case "readarray": {
|
|
329
|
+
let skipNext = false;
|
|
330
|
+
for (const argument of node.arguments) {
|
|
331
|
+
if (skipNext) {
|
|
332
|
+
skipNext = false;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const text = getStaticText(argument);
|
|
336
|
+
if (text === null) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (text.startsWith("-")) {
|
|
340
|
+
skipNext =
|
|
341
|
+
name === "read"
|
|
342
|
+
? READ_FLAGS_WITH_ARGUMENT.test(text)
|
|
343
|
+
: MAPFILE_FLAGS_WITH_ARGUMENT.test(text);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (IDENTIFIER.test(text)) {
|
|
347
|
+
addWrite(text, {
|
|
348
|
+
type: "Identifier",
|
|
349
|
+
start: argument.start,
|
|
350
|
+
end: argument.end,
|
|
351
|
+
name: text,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
// no default
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
"Program:exit"() {
|
|
361
|
+
if (unsafe) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
for (const [name, identifier] of writes) {
|
|
365
|
+
if (reads.has(name) ||
|
|
366
|
+
SPECIAL_VARIABLES.has(name) ||
|
|
367
|
+
name.startsWith("LC_") ||
|
|
368
|
+
allowedNames.has(name) ||
|
|
369
|
+
name.startsWith("_")) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
context.report({
|
|
373
|
+
node: identifier,
|
|
374
|
+
messageId: "unusedVariable",
|
|
375
|
+
data: { name },
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
export default rule;
|