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