@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.
Files changed (37) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +167 -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 +8 -0
  12. package/dist/rules/no-backticks.js +46 -0
  13. package/dist/rules/no-expansions-in-single-quotes.d.ts +8 -0
  14. package/dist/rules/no-expansions-in-single-quotes.js +42 -0
  15. package/dist/rules/no-ls-iteration.d.ts +8 -0
  16. package/dist/rules/no-ls-iteration.js +42 -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 +11 -0
  20. package/dist/rules/no-unused-vars.js +382 -0
  21. package/dist/rules/no-useless-cat.d.ts +8 -0
  22. package/dist/rules/no-useless-cat.js +76 -0
  23. package/dist/rules/no-useless-echo.d.ts +8 -0
  24. package/dist/rules/no-useless-echo.js +42 -0
  25. package/dist/rules/no-variables-in-printf-format.d.ts +8 -0
  26. package/dist/rules/no-variables-in-printf-format.js +60 -0
  27. package/dist/rules/require-cd-guard.d.ts +8 -0
  28. package/dist/rules/require-cd-guard.js +74 -0
  29. package/dist/rules/require-read-r.d.ts +8 -0
  30. package/dist/rules/require-read-r.js +83 -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 +75 -4
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Rule to disallow legacy backtick command substitution.
3
+ */
4
+ import type { ShellRuleDefinition } from "../types.js";
5
+ declare const rule: ShellRuleDefinition<{
6
+ MessageIds: "useDollarParen";
7
+ }>;
8
+ export default rule;
@@ -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,8 @@
1
+ /**
2
+ * @fileoverview Rule to flag expansion-like text inside single quotes.
3
+ */
4
+ import type { ShellRuleDefinition } from "../types.js";
5
+ declare const rule: ShellRuleDefinition<{
6
+ MessageIds: "expansionInSingleQuotes";
7
+ }>;
8
+ 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,8 @@
1
+ /**
2
+ * @fileoverview Rule to disallow iterating over `ls` output.
3
+ */
4
+ import type { ShellRuleDefinition } from "../types.js";
5
+ declare const rule: ShellRuleDefinition<{
6
+ MessageIds: "lsIteration";
7
+ }>;
8
+ 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;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Rule to disallow piping from a single-file `cat`.
3
+ */
4
+ import type { ShellRuleDefinition } from "../types.js";
5
+ declare const rule: ShellRuleDefinition<{
6
+ MessageIds: "uselessCat";
7
+ }>;
8
+ export default rule;