@eslint-sh/eslint-plugin 0.5.0 → 0.7.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 +228 -17
- package/dist/command-harvest/builtins/bash.d.ts +36 -0
- package/dist/command-harvest/builtins/bash.js +98 -0
- package/dist/command-harvest/builtins/bats.d.ts +45 -0
- package/dist/command-harvest/builtins/bats.js +33 -0
- package/dist/command-harvest/builtins/index.d.ts +18 -0
- package/dist/command-harvest/builtins/index.js +39 -0
- package/dist/command-harvest/builtins/posix.d.ts +51 -0
- package/dist/command-harvest/builtins/posix.js +93 -0
- package/dist/command-harvest/harvest-commands.d.ts +66 -0
- package/dist/command-harvest/harvest-commands.js +97 -0
- package/dist/command-harvest/wrapper-table.d.ts +24 -0
- package/dist/command-harvest/wrapper-table.js +49 -0
- package/dist/configs/agent-authored.d.ts +41 -0
- package/dist/configs/agent-authored.js +33 -0
- package/dist/configs/bash-3-2.d.ts +28 -0
- package/dist/configs/bash-3-2.js +51 -0
- package/dist/configs/posix-portable.d.ts +40 -0
- package/dist/configs/posix-portable.js +53 -0
- package/dist/configs/recommended.d.ts +52 -0
- package/dist/configs/recommended.js +34 -0
- package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
- package/dist/declaration-conventions/comment-pragma.js +153 -0
- package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
- package/dist/declaration-conventions/declaration-convention.js +1 -0
- package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
- package/dist/declaration-conventions/read-declared-commands.js +128 -0
- package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
- package/dist/eslint-plugin-syntax.d.ts +27 -0
- package/dist/eslint-plugin.d.ts +1211 -52
- package/dist/index.d.ts +56 -8
- package/dist/index.js +53 -6
- package/dist/language/command-resolution.d.ts +124 -0
- package/dist/language/command-resolution.js +120 -0
- package/dist/language/end-of-input-marker.d.ts +29 -0
- package/dist/language/end-of-input-marker.js +88 -0
- package/dist/language/line-breaks.d.ts +18 -0
- package/dist/language/line-breaks.js +22 -0
- package/dist/language/lint-view-types.d.ts +104 -0
- package/dist/language/lint-view-types.js +1 -0
- package/dist/language/non-shell-file-guard.d.ts +35 -0
- package/dist/language/non-shell-file-guard.js +59 -0
- package/dist/language/selector-classes.d.ts +0 -1
- package/dist/language/selector-classes.js +0 -1
- package/dist/language/shell-language-options.d.ts +7 -1
- package/dist/language/shell-language-options.js +4 -4
- package/dist/language/shell-language.d.ts +10 -2
- package/dist/language/shell-language.js +59 -8
- package/dist/language/shell-source-code.d.ts +342 -5
- package/dist/language/shell-source-code.js +559 -30
- package/dist/language/source-coordinates.d.ts +13 -2
- package/dist/language/source-coordinates.js +68 -14
- package/dist/language/syntax-view.d.ts +60 -39
- package/dist/language/syntax-view.js +200 -45
- package/dist/language/word-resolution.d.ts +4 -3
- package/dist/language/word-resolution.js +4 -3
- package/dist/rules/compat/create-compat-rule.d.ts +59 -0
- package/dist/rules/compat/create-compat-rule.js +65 -0
- package/dist/rules/compat/declaration-options.d.ts +19 -0
- package/dist/rules/compat/declaration-options.js +110 -0
- package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
- package/dist/rules/compat/no-append-all-redirect.js +73 -0
- package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
- package/dist/rules/compat/no-associative-arrays.js +39 -0
- package/dist/rules/compat/no-brace-sequence-padding-step.d.ts +15 -0
- package/dist/rules/compat/no-brace-sequence-padding-step.js +287 -0
- package/dist/rules/compat/no-case-attributes.d.ts +12 -0
- package/dist/rules/compat/no-case-attributes.js +63 -0
- package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
- package/dist/rules/compat/no-case-fallthrough.js +67 -0
- package/dist/rules/compat/no-case-modification.d.ts +35 -0
- package/dist/rules/compat/no-case-modification.js +86 -0
- package/dist/rules/compat/no-coproc.d.ts +12 -0
- package/dist/rules/compat/no-coproc.js +47 -0
- package/dist/rules/compat/no-declare-g.d.ts +12 -0
- package/dist/rules/compat/no-declare-g.js +59 -0
- package/dist/rules/compat/no-double-bracket.d.ts +12 -0
- package/dist/rules/compat/no-double-bracket.js +52 -0
- package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
- package/dist/rules/compat/no-epoch-variables.js +57 -0
- package/dist/rules/compat/no-fd-variable.d.ts +12 -0
- package/dist/rules/compat/no-fd-variable.js +48 -0
- package/dist/rules/compat/no-globstar.d.ts +12 -0
- package/dist/rules/compat/no-globstar.js +39 -0
- package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
- package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
- package/dist/rules/compat/no-let.d.ts +12 -0
- package/dist/rules/compat/no-let.js +62 -0
- package/dist/rules/compat/no-mapfile.d.ts +13 -0
- package/dist/rules/compat/no-mapfile.js +122 -0
- package/dist/rules/compat/no-nameref.d.ts +12 -0
- package/dist/rules/compat/no-nameref.js +44 -0
- package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
- package/dist/rules/compat/no-negative-array-index.js +123 -0
- package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
- package/dist/rules/compat/no-nounset-empty-array.js +158 -0
- package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
- package/dist/rules/compat/no-parameter-transformation.js +123 -0
- package/dist/rules/compat/no-pipe-all.d.ts +13 -0
- package/dist/rules/compat/no-pipe-all.js +68 -0
- package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
- package/dist/rules/compat/no-printf-time-format.js +99 -0
- package/dist/rules/compat/no-source.d.ts +13 -0
- package/dist/rules/compat/no-source.js +103 -0
- package/dist/rules/compat/no-test-v.d.ts +12 -0
- package/dist/rules/compat/no-test-v.js +280 -0
- package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
- package/dist/rules/compat/no-unicode-escape.js +120 -0
- package/dist/rules/compat/no-wait-n.d.ts +12 -0
- package/dist/rules/compat/no-wait-n.js +73 -0
- package/dist/rules/compat/operator-range.d.ts +10 -0
- package/dist/rules/compat/operator-range.js +15 -0
- package/dist/rules/compat/posix-absent-command.d.ts +12 -0
- package/dist/rules/compat/posix-absent-command.js +134 -0
- package/dist/rules/compat/shopt-options.d.ts +9 -0
- package/dist/rules/compat/shopt-options.js +63 -0
- package/dist/rules/compat/version-gates.d.ts +76 -0
- package/dist/rules/compat/version-gates.js +205 -0
- package/dist/rules/create-rule.d.ts +91 -0
- package/dist/rules/create-rule.js +51 -0
- package/dist/rules/create-wrapper-rule.d.ts +166 -0
- package/dist/rules/create-wrapper-rule.js +407 -0
- package/dist/rules/define-shell-rule.d.ts +184 -0
- package/dist/rules/define-shell-rule.js +56 -0
- package/dist/rules/index.d.ts +26 -0
- package/dist/rules/index.js +90 -0
- package/dist/rules/max-script-complexity.d.ts +28 -0
- package/dist/rules/max-script-complexity.js +130 -0
- package/dist/rules/no-dynamic-invocation.d.ts +35 -0
- package/dist/rules/no-dynamic-invocation.js +117 -0
- package/dist/rules/no-eval.d.ts +13 -0
- package/dist/rules/no-eval.js +89 -0
- package/dist/rules/no-masked-exit-status.d.ts +21 -0
- package/dist/rules/no-masked-exit-status.js +667 -0
- package/dist/rules/no-secret-env-reads.d.ts +24 -0
- package/dist/rules/no-secret-env-reads.js +133 -0
- package/dist/rules/no-secret-flags.d.ts +32 -0
- package/dist/rules/no-secret-flags.js +162 -0
- package/dist/rules/no-unchecked-cd.d.ts +44 -0
- package/dist/rules/no-unchecked-cd.js +250 -0
- package/dist/rules/no-undeclared-commands.d.ts +20 -0
- package/dist/rules/no-undeclared-commands.js +115 -0
- package/dist/rules/no-unused-declarations.d.ts +23 -0
- package/dist/rules/no-unused-declarations.js +259 -0
- package/dist/rules/prefer-local.d.ts +16 -0
- package/dist/rules/prefer-local.js +249 -0
- package/dist/rules/quote-expansions.d.ts +13 -0
- package/dist/rules/quote-expansions.js +229 -0
- package/dist/rules/require-network-wrapper.d.ts +40 -0
- package/dist/rules/require-network-wrapper.js +244 -0
- package/dist/rules/require-shebang.d.ts +29 -0
- package/dist/rules/require-shebang.js +101 -0
- package/dist/rules/require-strict-mode.d.ts +52 -0
- package/dist/rules/require-strict-mode.js +177 -0
- package/dist/rules/require-tty-guard.d.ts +13 -0
- package/dist/rules/require-tty-guard.js +318 -0
- package/dist/rules/resolve-declaration-convention.d.ts +96 -0
- package/dist/rules/resolve-declaration-convention.js +109 -0
- package/dist/rules/resolve-network-wrapper.d.ts +19 -0
- package/dist/rules/resolve-network-wrapper.js +116 -0
- package/dist/rules/set-options.d.ts +109 -0
- package/dist/rules/set-options.js +201 -0
- package/dist/rules/shebang-interpreter.d.ts +18 -0
- package/dist/rules/shebang-interpreter.js +81 -0
- package/dist/rules/shebang-matches-dialect.d.ts +12 -0
- package/dist/rules/shebang-matches-dialect.js +69 -0
- package/dist/rules/transparent-wrappers.d.ts +52 -0
- package/dist/rules/transparent-wrappers.js +99 -0
- package/dist/rules/user-pattern.d.ts +63 -0
- package/dist/rules/user-pattern.js +184 -0
- package/dist/syntax/analyze.d.ts +19 -0
- package/dist/syntax/analyze.js +21 -0
- package/dist/syntax/index.d.ts +23 -0
- package/dist/syntax/index.js +25 -0
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -1
- package/docs/compat.md +354 -0
- package/docs/custom-rule-tutorial.md +780 -34
- package/docs/declaration-conventions.md +478 -0
- package/docs/examples/audit-wrapper-pack.mjs +15 -0
- package/docs/examples/dep-line-convention.mjs +22 -0
- package/docs/examples/eslint.config.mjs +3 -2
- package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
- package/docs/examples/no-backticks-rule-tester.mjs +106 -0
- package/docs/examples/no-backticks-rule.mjs +60 -0
- package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
- package/docs/examples/no-function-keyword-rule.mjs +59 -0
- package/docs/examples/no-listed-commands-rule.ts +63 -0
- package/docs/examples/no-rm-rf-rule.mjs +26 -15
- package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
- package/docs/examples/sidecar-convention.mjs +38 -0
- package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
- package/docs/examples/sorted-declarations-rule.mjs +52 -0
- package/docs/reading-nodes.md +159 -0
- package/docs/rules/README.md +79 -0
- package/docs/rules/_template.md +83 -0
- package/docs/rules/compat/no-append-all-redirect.md +87 -0
- package/docs/rules/compat/no-associative-arrays.md +95 -0
- package/docs/rules/compat/no-brace-sequence-padding-step.md +90 -0
- package/docs/rules/compat/no-case-attributes.md +82 -0
- package/docs/rules/compat/no-case-fallthrough.md +90 -0
- package/docs/rules/compat/no-case-modification.md +74 -0
- package/docs/rules/compat/no-coproc.md +76 -0
- package/docs/rules/compat/no-declare-g.md +80 -0
- package/docs/rules/compat/no-double-bracket.md +72 -0
- package/docs/rules/compat/no-epoch-variables.md +79 -0
- package/docs/rules/compat/no-fd-variable.md +78 -0
- package/docs/rules/compat/no-globstar.md +74 -0
- package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
- package/docs/rules/compat/no-let.md +78 -0
- package/docs/rules/compat/no-mapfile.md +118 -0
- package/docs/rules/compat/no-nameref.md +81 -0
- package/docs/rules/compat/no-negative-array-index.md +87 -0
- package/docs/rules/compat/no-nounset-empty-array.md +103 -0
- package/docs/rules/compat/no-parameter-transformation.md +94 -0
- package/docs/rules/compat/no-pipe-all.md +85 -0
- package/docs/rules/compat/no-printf-time-format.md +75 -0
- package/docs/rules/compat/no-source.md +98 -0
- package/docs/rules/compat/no-test-v.md +82 -0
- package/docs/rules/compat/no-unicode-escape.md +91 -0
- package/docs/rules/compat/no-wait-n.md +85 -0
- package/docs/rules/max-script-complexity.md +109 -0
- package/docs/rules/no-dynamic-invocation.md +139 -0
- package/docs/rules/no-eval.md +106 -0
- package/docs/rules/no-masked-exit-status.md +239 -0
- package/docs/rules/no-secret-env-reads.md +173 -0
- package/docs/rules/no-secret-flags.md +169 -0
- package/docs/rules/no-unchecked-cd.md +146 -0
- package/docs/rules/no-undeclared-commands.md +141 -0
- package/docs/rules/no-unused-declarations.md +216 -0
- package/docs/rules/prefer-local.md +221 -0
- package/docs/rules/quote-expansions.md +157 -0
- package/docs/rules/require-network-wrapper.md +171 -0
- package/docs/rules/require-shebang.md +157 -0
- package/docs/rules/require-strict-mode.md +253 -0
- package/docs/rules/require-tty-guard.md +131 -0
- package/docs/rules/shebang-matches-dialect.md +120 -0
- package/docs/selectors.md +2 -2
- package/docs/shellcheck.md +87 -0
- package/docs/untrusted-code.md +440 -0
- package/docs/wrapper-rules.md +206 -0
- package/package.json +26 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/language/selector-classes.d.ts.map +0 -1
- package/dist/language/selector-classes.js.map +0 -1
- package/dist/language/shell-language-options.d.ts.map +0 -1
- package/dist/language/shell-language-options.js.map +0 -1
- package/dist/language/shell-language.d.ts.map +0 -1
- package/dist/language/shell-language.js.map +0 -1
- package/dist/language/shell-source-code.d.ts.map +0 -1
- package/dist/language/shell-source-code.js.map +0 -1
- package/dist/language/source-coordinates.d.ts.map +0 -1
- package/dist/language/source-coordinates.js.map +0 -1
- package/dist/language/syntax-view.d.ts.map +0 -1
- package/dist/language/syntax-view.js.map +0 -1
- package/dist/language/word-resolution.d.ts.map +0 -1
- package/dist/language/word-resolution.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-negative-array-index`: reports negative indexed-array
|
|
3
|
+
* subscripts written as a literal (`${arr[-1]}`, `arr[-2]=x`, `${#arr[-1]}`)
|
|
4
|
+
* in files whose target shell lacks them, that is, Bash older than 4.3
|
|
5
|
+
* (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
|
|
6
|
+
*
|
|
7
|
+
* Bash 3.2 evaluates `-1` as the arithmetic value minus one and fails with
|
|
8
|
+
* "bad array subscript", so the read yields nothing and the assignment is
|
|
9
|
+
* refused. The message suggests the portable alternative: count from the
|
|
10
|
+
* array length, `${arr[$((${#arr[@]}-1))]}`.
|
|
11
|
+
*
|
|
12
|
+
* The suggestion rewrites exactly a bare `${NAME[-N]}` read to
|
|
13
|
+
* `${NAME[$((${#NAME[@]}-N))]}`. Negative subscripts count back from the
|
|
14
|
+
* highest assigned index plus one, while `${#NAME[@]}` counts set elements,
|
|
15
|
+
* so the two agree only for arrays without holes; the suggestion message says
|
|
16
|
+
* so. Assignments and other expansion forms are reported without a suggestion.
|
|
17
|
+
*
|
|
18
|
+
* What is read: an `Index` that is a unary minus applied to a static decimal
|
|
19
|
+
* integer, on a `ParamExp` or an `Assign`. A computed subscript such as
|
|
20
|
+
* `-$n` is not a literal and is not reported, and neither is `-0`, which is
|
|
21
|
+
* zero. A `ParamExp` or `Assign` subscript is read from the syntax tree, so
|
|
22
|
+
* its operand can be any arithmetic node (`-(1)` holds a parenthesized one);
|
|
23
|
+
* only a plain word is resolved.
|
|
24
|
+
*
|
|
25
|
+
* Unsetting an element (`unset arr[-1]`, quoted or not) is reported from the
|
|
26
|
+
* `unset` command's operand text, since the parser keeps that operand as an
|
|
27
|
+
* ordinary word; option words (`-v`) never match an element name.
|
|
28
|
+
*/
|
|
29
|
+
import { Minus } from 'sh-ast';
|
|
30
|
+
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
31
|
+
import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
|
|
32
|
+
import { createCompatRule } from './create-compat-rule.js';
|
|
33
|
+
/** A bare negative-subscript read, the only shape the suggestion rewrites. */
|
|
34
|
+
const BARE_READ = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\[\s*-\s*(\d+)\s*\]\}$/u;
|
|
35
|
+
/**
|
|
36
|
+
* The wrappers followed to find an `unset` command: `sh-ast/analyze`'s
|
|
37
|
+
* defaults (which include `command`) plus Bash's `builtin`.
|
|
38
|
+
*/
|
|
39
|
+
const TRANSPARENT_WRAPPERS = [
|
|
40
|
+
...DEFAULT_TRANSPARENT_WRAPPERS,
|
|
41
|
+
BUILTIN_WRAPPER,
|
|
42
|
+
];
|
|
43
|
+
/** An `unset` operand naming an element by a literal negative subscript. */
|
|
44
|
+
const UNSET_NEGATIVE = /^[A-Za-z_][A-Za-z0-9_]*\[\s*-\s*\d+\s*\]$/u;
|
|
45
|
+
/**
|
|
46
|
+
* The `sh/compat/no-negative-array-index` rule module.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export const noNegativeArrayIndex = createCompatRule({
|
|
51
|
+
name: 'compat/no-negative-array-index',
|
|
52
|
+
feature: 'negativeArrayIndex',
|
|
53
|
+
meta: {
|
|
54
|
+
type: 'problem',
|
|
55
|
+
hasSuggestions: true,
|
|
56
|
+
docs: {
|
|
57
|
+
description: 'Disallow negative array subscripts (`${arr[-1]}`) when the target shell lacks them',
|
|
58
|
+
},
|
|
59
|
+
messages: {
|
|
60
|
+
negativeIndex: 'Negative array subscripts need {{required}}, but this file targets {{target}}. Count from the length instead, for example `${arr[$((${#arr[@]}-1))]}`.',
|
|
61
|
+
useArrayLength: 'Replace with `${{{name}}[$((${#{{name}}[@]}-{{n}}))]}` (the two differ only for an array with unset elements).',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
create(context, gate) {
|
|
65
|
+
const { sourceCode } = context;
|
|
66
|
+
/** Whether `index` is a unary minus on a static decimal integer. */
|
|
67
|
+
function isNegativeLiteral(index) {
|
|
68
|
+
if (index?.type !== 'UnaryArithm' || index.op !== Minus)
|
|
69
|
+
return false;
|
|
70
|
+
const operand = index.x;
|
|
71
|
+
// The operand may be any arithmetic node (`-(1)` holds a ParenArithm);
|
|
72
|
+
// only a Word can be resolved, and `-0` is zero, not a negative index.
|
|
73
|
+
if (operand?.type !== 'Word')
|
|
74
|
+
return false;
|
|
75
|
+
const digits = sourceCode.resolveWordText(operand) ?? '';
|
|
76
|
+
return /^\d+$/u.test(digits) && /[1-9]/u.test(digits);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
CallExpr(node) {
|
|
80
|
+
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
81
|
+
transparentWrappers: TRANSPARENT_WRAPPERS,
|
|
82
|
+
});
|
|
83
|
+
if (invoked?.effective.static !== true || invoked.effective.text !== 'unset')
|
|
84
|
+
return;
|
|
85
|
+
// `chainIndices` index `node.args` directly, so slice before filtering.
|
|
86
|
+
for (const word of node.args.slice((invoked.chainIndices.at(-1) ?? 0) + 1)) {
|
|
87
|
+
// An unquoted `a[-1]` holds a glob bracket, so it is not "static"; its
|
|
88
|
+
// written text is what names the element.
|
|
89
|
+
const text = sourceCode.resolveWordText(word) ?? sourceCode.getText(word);
|
|
90
|
+
if (text === '--')
|
|
91
|
+
continue;
|
|
92
|
+
if (UNSET_NEGATIVE.test(text) && /[1-9]/u.test(text)) {
|
|
93
|
+
context.report({ node: word, messageId: 'negativeIndex', data: { ...gate } });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
ParamExp(node) {
|
|
98
|
+
if (!isNegativeLiteral(node.index))
|
|
99
|
+
return;
|
|
100
|
+
const match = BARE_READ.exec(sourceCode.getText(node));
|
|
101
|
+
context.report({
|
|
102
|
+
node,
|
|
103
|
+
messageId: 'negativeIndex',
|
|
104
|
+
data: { ...gate },
|
|
105
|
+
suggest: match === null
|
|
106
|
+
? []
|
|
107
|
+
: [
|
|
108
|
+
{
|
|
109
|
+
messageId: 'useArrayLength',
|
|
110
|
+
data: { name: match[1], n: match[2] },
|
|
111
|
+
fix: (fixer) => fixer.replaceText(node, `\${${match[1]}[$((\${#${match[1]}[@]}-${match[2]}))]}`),
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
Assign(node) {
|
|
117
|
+
if (!isNegativeLiteral(node.index))
|
|
118
|
+
return;
|
|
119
|
+
context.report({ node, messageId: 'negativeIndex', data: { ...gate } });
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with: `emptyArray` for the report and
|
|
3
|
+
* `guardExpansion` for its suggestion.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export type NoNounsetEmptyArrayMessageId = 'emptyArray' | 'guardExpansion';
|
|
8
|
+
/**
|
|
9
|
+
* The `sh/compat/no-nounset-empty-array` rule module.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare const noNounsetEmptyArray: import("../define-shell-rule.js").ShellRule<[], NoNounsetEmptyArrayMessageId>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-nounset-empty-array`: reports whole-array expansions
|
|
3
|
+
* (`"${a[@]}"`, `${a[*]}`) in a file that turns on `set -u`, when its target
|
|
4
|
+
* shell is Bash older than 4.4 (macOS's stock `/bin/bash` is 3.2) or POSIX
|
|
5
|
+
* sh.
|
|
6
|
+
*
|
|
7
|
+
* The syntax is in every Bash, but through 4.3 expanding an empty array
|
|
8
|
+
* while `nounset` is on aborts the script with "unbound variable". Bash 4.4
|
|
9
|
+
* is the first release where it expands to nothing. This is the classic
|
|
10
|
+
* "works on Linux CI, breaks on a Mac" failure for a script that passes an
|
|
11
|
+
* optional, possibly empty, argument array under `set -euo pipefail`.
|
|
12
|
+
*
|
|
13
|
+
* The suggestion rewrites a word that is exactly `"${a[@]}"` to
|
|
14
|
+
* `${a[@]+"${a[@]}"}`, which expands to nothing for an empty array and to the
|
|
15
|
+
* same words otherwise, and a word that is exactly `"${a[*]}"` to
|
|
16
|
+
* `"${a[*]-}"`, which keeps the one (empty) word `"${a[*]}"` gives for an
|
|
17
|
+
* empty array. Other spellings (unquoted, or inside a longer word) are
|
|
18
|
+
* reported without one. POSIX sh has no arrays at all; under
|
|
19
|
+
* `dialect: "posix"` the parser rejects array syntax, so the rule never sees
|
|
20
|
+
* one there.
|
|
21
|
+
*
|
|
22
|
+
* What is read:
|
|
23
|
+
*
|
|
24
|
+
* - whether the file turns `nounset` on anywhere: a `set` command that
|
|
25
|
+
* enables it (`set -u`, `set -eu`, `set -o nounset`), or a shebang that
|
|
26
|
+
* passes the flag (`#!/bin/bash -eu`). Where in the file that happens is
|
|
27
|
+
* not considered, since a function defined before `set -u` can run after
|
|
28
|
+
* it;
|
|
29
|
+
* - every `${name[@]}` and `${name[*]}` with no operator, except inside
|
|
30
|
+
* `${name[@]+…}` or `${name[@]:+…}` of the same array (the guard this rule
|
|
31
|
+
* suggests), whose word Bash expands only when the array is set. `${#a[@]}`,
|
|
32
|
+
* `${!a[@]}` and `${a[@]-default}` work on an empty array under `set -u` in
|
|
33
|
+
* every Bash, so they are not reported. The slice `${a[@]:1}` and the
|
|
34
|
+
* replacement `${a[@]/x/y}` were observed to work on Bash 3.2 to 4.1 and
|
|
35
|
+
* fail only on 4.2 and 4.3, which a minimum-version gate cannot express, so
|
|
36
|
+
* they are not reported either.
|
|
37
|
+
*/
|
|
38
|
+
import { AlternateUnset, AlternateUnsetOrNull } from 'sh-ast';
|
|
39
|
+
import { createCompatRule } from './create-compat-rule.js';
|
|
40
|
+
import { parseSetArguments, shebangOptionChanges } from '../set-options.js';
|
|
41
|
+
/** A shell variable name. */
|
|
42
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
43
|
+
/**
|
|
44
|
+
* The guarded spelling of `"${name[subscript]}"`: `${name[@]+"${name[@]}"}`
|
|
45
|
+
* for `@` (no words for an empty array, the same words otherwise) and
|
|
46
|
+
* `"${name[*]-}"` for `*` (one empty word for an empty array, as Bash 4.4
|
|
47
|
+
* gives, and the same joined word otherwise).
|
|
48
|
+
*/
|
|
49
|
+
function guardedExpansion(name, subscript) {
|
|
50
|
+
return subscript === '@' ? `\${${name}[@]+"\${${name}[@]}"}` : `"\${${name}[*]-}"`;
|
|
51
|
+
}
|
|
52
|
+
/** Whether `changes` turn `nounset` on. */
|
|
53
|
+
function enablesNounset(changes) {
|
|
54
|
+
return changes.some((change) => change.option === 'nounset' && change.enable);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The `sh/compat/no-nounset-empty-array` rule module.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export const noNounsetEmptyArray = createCompatRule({
|
|
62
|
+
name: 'compat/no-nounset-empty-array',
|
|
63
|
+
feature: 'nounsetEmptyArray',
|
|
64
|
+
meta: {
|
|
65
|
+
type: 'problem',
|
|
66
|
+
hasSuggestions: true,
|
|
67
|
+
docs: {
|
|
68
|
+
description: 'Disallow whole-array expansions under `set -u` when the target shell aborts on an empty array',
|
|
69
|
+
},
|
|
70
|
+
messages: {
|
|
71
|
+
emptyArray: 'If this array is empty, `${{{name}}[{{subscript}}]}` under `set -u` needs {{required}}, but this file targets {{target}}, where it aborts with "unbound variable". Guard it, for example `{{example}}`.',
|
|
72
|
+
guardExpansion: 'Replace with `{{replacement}}`.',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
create(context, gate) {
|
|
76
|
+
const { sourceCode } = context;
|
|
77
|
+
let nounset = enablesNounset(shebangOptionChanges(sourceCode.text));
|
|
78
|
+
const candidates = [];
|
|
79
|
+
/** The `@` or `*` of a whole-array subscript, or `undefined`. */
|
|
80
|
+
function wholeArraySubscript(index) {
|
|
81
|
+
if (index?.type !== 'Word')
|
|
82
|
+
return undefined;
|
|
83
|
+
const text = sourceCode.resolveWordText(index) ?? sourceCode.getText(index);
|
|
84
|
+
return text === '@' || text === '*' ? text : undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether `node` sits inside `${name[…]+…}` or `${name[…]:+…}` of the same
|
|
88
|
+
* array, as in the guard `${a[@]+"${a[@]}"}`: Bash expands that inner
|
|
89
|
+
* word only when the array is set, so it cannot abort.
|
|
90
|
+
*/
|
|
91
|
+
function insideOwnGuard(node, name) {
|
|
92
|
+
for (let up = sourceCode.getParent(node); up !== undefined; up = sourceCode.getParent(up)) {
|
|
93
|
+
if (up.type === 'ParamExp' &&
|
|
94
|
+
up.param?.value === name &&
|
|
95
|
+
(up.exp?.op === AlternateUnset || up.exp?.op === AlternateUnsetOrNull)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The quoted expansion `node` is when it is the whole of a word that is
|
|
103
|
+
* exactly `"${name[@]}"` or `"${name[*]}"`, or `undefined`.
|
|
104
|
+
*/
|
|
105
|
+
function wholeQuotedWord(node) {
|
|
106
|
+
const quoted = sourceCode.getParent(node);
|
|
107
|
+
if (quoted?.type !== 'DblQuoted' || quoted.parts.length !== 1)
|
|
108
|
+
return undefined;
|
|
109
|
+
const word = sourceCode.getParent(quoted);
|
|
110
|
+
return word?.type === 'Word' && word.parts.length === 1 ? quoted : undefined;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
CallExpr(node) {
|
|
114
|
+
if (sourceCode.getCommandName(node) !== 'set')
|
|
115
|
+
return;
|
|
116
|
+
const args = node.args.slice(1).map((word) => sourceCode.resolveWordText(word));
|
|
117
|
+
if (enablesNounset(parseSetArguments(args).changes))
|
|
118
|
+
nounset = true;
|
|
119
|
+
},
|
|
120
|
+
ParamExp(node) {
|
|
121
|
+
const name = node.param?.value;
|
|
122
|
+
if (name === undefined || !IDENTIFIER.test(name))
|
|
123
|
+
return;
|
|
124
|
+
if (node.exp !== undefined || node.slice !== undefined || node.repl !== undefined)
|
|
125
|
+
return;
|
|
126
|
+
if (node.length || node.excl || node.width || node.isset || node.modifiers.length > 0) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const subscript = wholeArraySubscript(node.index);
|
|
130
|
+
if (subscript === undefined || insideOwnGuard(node, name))
|
|
131
|
+
return;
|
|
132
|
+
candidates.push({ node, name, subscript });
|
|
133
|
+
},
|
|
134
|
+
'File:exit'() {
|
|
135
|
+
if (!nounset)
|
|
136
|
+
return;
|
|
137
|
+
for (const { node, name, subscript } of candidates) {
|
|
138
|
+
const replacement = guardedExpansion(name, subscript);
|
|
139
|
+
const quoted = wholeQuotedWord(node);
|
|
140
|
+
context.report({
|
|
141
|
+
node,
|
|
142
|
+
messageId: 'emptyArray',
|
|
143
|
+
data: { ...gate, name, subscript, example: replacement },
|
|
144
|
+
suggest: quoted === undefined
|
|
145
|
+
? []
|
|
146
|
+
: [
|
|
147
|
+
{
|
|
148
|
+
messageId: 'guardExpansion',
|
|
149
|
+
data: { replacement },
|
|
150
|
+
fix: (fixer) => fixer.replaceText(quoted, replacement),
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with: `transformation` names an alternative,
|
|
3
|
+
* `transformationNoAlternative` has none to offer.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export type NoParameterTransformationMessageId = 'transformation' | 'transformationNoAlternative';
|
|
8
|
+
/**
|
|
9
|
+
* The `sh/compat/no-parameter-transformation` rule module.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare const noParameterTransformation: import("../define-shell-rule.js").ShellRule<[], NoParameterTransformationMessageId>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-parameter-transformation`: reports the parameter
|
|
3
|
+
* transformations `${v@op}` in files whose target shell lacks the operator.
|
|
4
|
+
* The minimum Bash depends on the operator:
|
|
5
|
+
*
|
|
6
|
+
* - `@Q`, `@E`, `@P`, `@A`, `@a`: Bash 4.4;
|
|
7
|
+
* - `@U`, `@u`, `@L` and `@K`: Bash 5.1;
|
|
8
|
+
* - `@k`: Bash 5.2.
|
|
9
|
+
*
|
|
10
|
+
* POSIX sh has none of them. An older Bash fails with "bad substitution"
|
|
11
|
+
* when the word is expanded. The message names a portable alternative where
|
|
12
|
+
* one exists for the target: `printf '%q'` for `@Q` (it quotes differently,
|
|
13
|
+
* but `eval` reads it back as the same value), `printf '%b'` for `@E`,
|
|
14
|
+
* `declare -p` for `@A`, and for the case operators the Bash 4.0 case
|
|
15
|
+
* expansions (`${v^^}`, `${v^}`, `${v,,}`) on a Bash that has them, or `tr`
|
|
16
|
+
* otherwise. `printf '%q'` and `declare -p` are not POSIX, so a POSIX target
|
|
17
|
+
* is not offered them.
|
|
18
|
+
*
|
|
19
|
+
* What is read: a `ParamExp` whose expansion operator is `@` followed by one
|
|
20
|
+
* of the letters above. The report is on the whole expansion.
|
|
21
|
+
*/
|
|
22
|
+
import { OtherParamOps } from 'sh-ast';
|
|
23
|
+
import { createMultiFeatureCompatRule } from './create-compat-rule.js';
|
|
24
|
+
import { TO_LOWER, TO_UPPER, trExample } from './no-case-modification.js';
|
|
25
|
+
import { isFeatureAvailable } from './version-gates.js';
|
|
26
|
+
/** Each operator letter's gate-table row. */
|
|
27
|
+
const OPERATOR_FEATURES = new Map([
|
|
28
|
+
['Q', 'parameterTransformation'],
|
|
29
|
+
['E', 'parameterTransformation'],
|
|
30
|
+
['P', 'parameterTransformation'],
|
|
31
|
+
['A', 'parameterTransformation'],
|
|
32
|
+
['a', 'parameterTransformation'],
|
|
33
|
+
['U', 'parameterTransformationCase'],
|
|
34
|
+
['u', 'parameterTransformationCase'],
|
|
35
|
+
['L', 'parameterTransformationCase'],
|
|
36
|
+
['K', 'parameterTransformationQuotedKeys'],
|
|
37
|
+
['k', 'parameterTransformationKeys'],
|
|
38
|
+
]);
|
|
39
|
+
/** The case operators and their equivalents. */
|
|
40
|
+
const CASE_EQUIVALENTS = new Map([
|
|
41
|
+
['U', { expansion: '^^', upper: true, all: true }],
|
|
42
|
+
['u', { expansion: '^', upper: true, all: false }],
|
|
43
|
+
['L', { expansion: ',,', upper: false, all: true }],
|
|
44
|
+
]);
|
|
45
|
+
/**
|
|
46
|
+
* The `sh/compat/no-parameter-transformation` rule module.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export const noParameterTransformation = createMultiFeatureCompatRule({
|
|
51
|
+
name: 'compat/no-parameter-transformation',
|
|
52
|
+
features: [...new Set(OPERATOR_FEATURES.values())],
|
|
53
|
+
meta: {
|
|
54
|
+
type: 'problem',
|
|
55
|
+
docs: {
|
|
56
|
+
description: 'Disallow parameter transformations (`${v@Q}`, `${v@U}` and the others) when the target shell lacks the operator',
|
|
57
|
+
},
|
|
58
|
+
messages: {
|
|
59
|
+
transformation: '`${…@{{operator}}}` needs {{required}}, but this file targets {{target}}. Use {{alternative}} instead.',
|
|
60
|
+
transformationNoAlternative: '`${…@{{operator}}}` needs {{required}}, but this file targets {{target}}, which has no equivalent expansion; restructure the code that needs it.',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
create(context, gates) {
|
|
64
|
+
const { sourceCode, languageOptions } = context;
|
|
65
|
+
const posix = sourceCode.dialect === 'posix';
|
|
66
|
+
const caseExpansions = isFeatureAvailable('caseModification', languageOptions);
|
|
67
|
+
/** The alternative shown for `operator` on `name`, or `undefined`. */
|
|
68
|
+
function alternative(operator, name) {
|
|
69
|
+
const value = `"$${name}"`;
|
|
70
|
+
if (operator === 'Q') {
|
|
71
|
+
return posix
|
|
72
|
+
? undefined
|
|
73
|
+
: `\`printf '%q' ${value}\` (it quotes differently, but \`eval\` reads it back as the same value)`;
|
|
74
|
+
}
|
|
75
|
+
if (operator === 'E')
|
|
76
|
+
return `\`printf '%b' ${value}\``;
|
|
77
|
+
if (operator === 'A') {
|
|
78
|
+
return posix
|
|
79
|
+
? undefined
|
|
80
|
+
: `\`declare -p ${name}\` (it prints the declaration; capture it and \`eval\` it to re-create the variable)`;
|
|
81
|
+
}
|
|
82
|
+
const equivalent = CASE_EQUIVALENTS.get(operator);
|
|
83
|
+
if (equivalent === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (caseExpansions)
|
|
86
|
+
return `\`"\${${name}${equivalent.expansion}}"\``;
|
|
87
|
+
const example = trExample(name, {
|
|
88
|
+
operator: equivalent.expansion,
|
|
89
|
+
all: equivalent.all,
|
|
90
|
+
tr: equivalent.upper ? TO_UPPER : TO_LOWER,
|
|
91
|
+
});
|
|
92
|
+
return `\`tr\`, for example ${example}`;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
ParamExp(node) {
|
|
96
|
+
const { exp } = node;
|
|
97
|
+
if (exp?.op !== OtherParamOps || exp.word === undefined)
|
|
98
|
+
return;
|
|
99
|
+
const operator = sourceCode.resolveWordText(exp.word);
|
|
100
|
+
if (operator === null)
|
|
101
|
+
return;
|
|
102
|
+
const feature = OPERATOR_FEATURES.get(operator);
|
|
103
|
+
const gate = feature === undefined ? undefined : gates.get(feature);
|
|
104
|
+
if (gate === undefined)
|
|
105
|
+
return;
|
|
106
|
+
const advice = alternative(operator, node.param?.value ?? 'v');
|
|
107
|
+
if (advice === undefined) {
|
|
108
|
+
context.report({
|
|
109
|
+
node,
|
|
110
|
+
messageId: 'transformationNoAlternative',
|
|
111
|
+
data: { ...gate, operator },
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
context.report({
|
|
116
|
+
node,
|
|
117
|
+
messageId: 'transformation',
|
|
118
|
+
data: { ...gate, operator, alternative: advice },
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with: `pipeAll` for the report and
|
|
3
|
+
* `useStderrRedirect` for its suggestion.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export type NoPipeAllMessageId = 'pipeAll' | 'useStderrRedirect';
|
|
8
|
+
/**
|
|
9
|
+
* The `sh/compat/no-pipe-all` rule module.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare const noPipeAll: import("../define-shell-rule.js").ShellRule<[], NoPipeAllMessageId>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-pipe-all`: reports the `|&` pipe, shorthand for `2>&1 |`, in
|
|
3
|
+
* files whose target shell lacks it, that is, Bash older than 4.0 (macOS's
|
|
4
|
+
* stock `/bin/bash` is 3.2) and POSIX sh.
|
|
5
|
+
*
|
|
6
|
+
* Bash 3.2 rejects `|&` as a syntax error, so the whole script fails to run.
|
|
7
|
+
* The portable spelling is `cmd 2>&1 | other`. The Bash manual defines `|&`
|
|
8
|
+
* as that implicit `2>&1`, applied after any redirections the left-hand
|
|
9
|
+
* command writes, so writing `2>&1` just before the `|` is exactly
|
|
10
|
+
* equivalent. The rule offers that rewrite as a suggestion.
|
|
11
|
+
*
|
|
12
|
+
* What is read: a pipeline (`BinaryCmd`) whose operator is `|&`. A plain `|`,
|
|
13
|
+
* `||`, `&&`, a background `&`, and any of these inside quotes are other
|
|
14
|
+
* syntax and are not reported. The report is on the `|&` operator.
|
|
15
|
+
*/
|
|
16
|
+
import { PipeAll } from 'sh-ast';
|
|
17
|
+
import { createCompatRule } from './create-compat-rule.js';
|
|
18
|
+
import { operatorRange } from './operator-range.js';
|
|
19
|
+
/**
|
|
20
|
+
* The `sh/compat/no-pipe-all` rule module.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export const noPipeAll = createCompatRule({
|
|
25
|
+
name: 'compat/no-pipe-all',
|
|
26
|
+
feature: 'pipeAll',
|
|
27
|
+
meta: {
|
|
28
|
+
type: 'problem',
|
|
29
|
+
hasSuggestions: true,
|
|
30
|
+
docs: {
|
|
31
|
+
description: 'Disallow the `|&` pipe when the target shell lacks it',
|
|
32
|
+
},
|
|
33
|
+
messages: {
|
|
34
|
+
pipeAll: '`|&` needs {{required}}, but this file targets {{target}}. Write `cmd 2>&1 | other` instead.',
|
|
35
|
+
useStderrRedirect: 'Replace `|&` with `2>&1 |`.',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
create(context, gate) {
|
|
39
|
+
const { sourceCode } = context;
|
|
40
|
+
return {
|
|
41
|
+
BinaryCmd(node) {
|
|
42
|
+
if (node.op !== PipeAll)
|
|
43
|
+
return;
|
|
44
|
+
const range = operatorRange(sourceCode, node, '|&');
|
|
45
|
+
if (range === undefined) {
|
|
46
|
+
context.report({ node, messageId: 'pipeAll', data: { ...gate } });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const [start, end] = range;
|
|
50
|
+
// `cmd|&other` has no space before the operator, and `2>&1` must not
|
|
51
|
+
// join the command's last word.
|
|
52
|
+
const before = sourceCode.text[start - 1];
|
|
53
|
+
const separator = before === ' ' || before === '\t' ? '' : ' ';
|
|
54
|
+
context.report({
|
|
55
|
+
loc: { start: sourceCode.getLocFromIndex(start), end: sourceCode.getLocFromIndex(end) },
|
|
56
|
+
messageId: 'pipeAll',
|
|
57
|
+
data: { ...gate },
|
|
58
|
+
suggest: [
|
|
59
|
+
{
|
|
60
|
+
messageId: 'useStderrRedirect',
|
|
61
|
+
fix: (fixer) => fixer.replaceTextRange(range, `${separator}2>&1 |`),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export type NoPrintfTimeFormatMessageId = 'printfTime';
|
|
7
|
+
/**
|
|
8
|
+
* The `sh/compat/no-printf-time-format` rule module.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const noPrintfTimeFormat: import("../define-shell-rule.js").ShellRule<[], "printfTime">;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sh/compat/no-printf-time-format`: reports `printf`'s `%(fmt)T` date
|
|
3
|
+
* format in files whose target shell lacks it, that is, Bash older than 4.2
|
|
4
|
+
* (macOS's stock `/bin/bash` is 3.2) and POSIX sh.
|
|
5
|
+
*
|
|
6
|
+
* Bash 4.1 rejects the directive with "invalid format character", so
|
|
7
|
+
* nothing is printed. The message suggests the portable alternative for the
|
|
8
|
+
* current time: `date '+fmt'`, which takes the same `strftime` format.
|
|
9
|
+
*
|
|
10
|
+
* What is read: a `printf` command (after transparent wrappers such as
|
|
11
|
+
* `builtin`) whose format argument, after an optional `-v name` and `--`, is
|
|
12
|
+
* a static word containing a `%(…)T` directive. The format is scanned as
|
|
13
|
+
* `printf` reads it: `%%` is a literal percent sign, and a directive may
|
|
14
|
+
* carry flags, a width and a precision before the `(`. A format that is not
|
|
15
|
+
* static cannot be read, and is not reported. The report is on the format
|
|
16
|
+
* word.
|
|
17
|
+
*/
|
|
18
|
+
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
19
|
+
import { BUILTIN_WRAPPER } from '../transparent-wrappers.js';
|
|
20
|
+
import { createCompatRule } from './create-compat-rule.js';
|
|
21
|
+
/**
|
|
22
|
+
* The wrappers followed to find the `printf` command: `sh-ast/analyze`'s
|
|
23
|
+
* defaults (which include `command`) plus Bash's `builtin`.
|
|
24
|
+
*/
|
|
25
|
+
const TRANSPARENT_WRAPPERS = [
|
|
26
|
+
...DEFAULT_TRANSPARENT_WRAPPERS,
|
|
27
|
+
BUILTIN_WRAPPER,
|
|
28
|
+
];
|
|
29
|
+
/** A conversion's flags, width and precision, as `printf` accepts them. */
|
|
30
|
+
const DIRECTIVE_PREFIX = /^[-+ #0']*(?:\d+|\*)?(?:\.(?:\d+|\*)?)?/u;
|
|
31
|
+
/**
|
|
32
|
+
* The `strftime` format of the first `%(fmt)T` directive in a `printf`
|
|
33
|
+
* format string, or `undefined` when it has none.
|
|
34
|
+
*/
|
|
35
|
+
function timeFormatIn(format) {
|
|
36
|
+
for (let index = 0; index < format.length; index++) {
|
|
37
|
+
if (format[index] !== '%')
|
|
38
|
+
continue;
|
|
39
|
+
if (format[index + 1] === '%') {
|
|
40
|
+
index++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const rest = format.slice(index + 1);
|
|
44
|
+
const prefix = DIRECTIVE_PREFIX.exec(rest)?.[0] ?? '';
|
|
45
|
+
const afterPrefix = rest.slice(prefix.length);
|
|
46
|
+
if (!afterPrefix.startsWith('('))
|
|
47
|
+
continue;
|
|
48
|
+
const close = afterPrefix.indexOf(')');
|
|
49
|
+
if (close !== -1 && afterPrefix[close + 1] === 'T')
|
|
50
|
+
return afterPrefix.slice(1, close);
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The `sh/compat/no-printf-time-format` rule module.
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export const noPrintfTimeFormat = createCompatRule({
|
|
60
|
+
name: 'compat/no-printf-time-format',
|
|
61
|
+
feature: 'printfTimeFormat',
|
|
62
|
+
meta: {
|
|
63
|
+
type: 'problem',
|
|
64
|
+
docs: {
|
|
65
|
+
description: "Disallow `printf`'s `%(fmt)T` date format when the target shell lacks it",
|
|
66
|
+
},
|
|
67
|
+
messages: {
|
|
68
|
+
printfTime: "`printf '%({{format}})T'` needs {{required}}, but this file targets {{target}}. For the current time, run `date '+{{format}}'` instead.",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
create(context, gate) {
|
|
72
|
+
const { sourceCode } = context;
|
|
73
|
+
return {
|
|
74
|
+
CallExpr(node) {
|
|
75
|
+
const invoked = sourceCode.resolveInvokedCommand(node, {
|
|
76
|
+
transparentWrappers: TRANSPARENT_WRAPPERS,
|
|
77
|
+
});
|
|
78
|
+
if (invoked?.effective.static !== true || invoked.effective.text !== 'printf')
|
|
79
|
+
return;
|
|
80
|
+
// `chainIndices` index `node.args` directly, so slice before filtering.
|
|
81
|
+
const words = node.args.slice((invoked.chainIndices.at(-1) ?? 0) + 1);
|
|
82
|
+
let index = 0;
|
|
83
|
+
const first = words.at(0);
|
|
84
|
+
if (first !== undefined && sourceCode.resolveWordText(first) === '-v')
|
|
85
|
+
index = 2;
|
|
86
|
+
if (index < words.length && sourceCode.resolveWordText(words[index]) === '--')
|
|
87
|
+
index++;
|
|
88
|
+
const formatWord = words.at(index);
|
|
89
|
+
if (formatWord === undefined)
|
|
90
|
+
return;
|
|
91
|
+
const text = sourceCode.resolveWordText(formatWord);
|
|
92
|
+
const format = text === null ? undefined : timeFormatIn(text);
|
|
93
|
+
if (format === undefined)
|
|
94
|
+
return;
|
|
95
|
+
context.report({ node: formatWord, messageId: 'printfTime', data: { ...gate, format } });
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message ids this rule reports with: `source` for the report and `useDot` for
|
|
3
|
+
* its suggestion.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export type NoSourceMessageId = 'source' | 'useDot';
|
|
8
|
+
/**
|
|
9
|
+
* The `sh/compat/no-source` rule module.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare const noSource: import("../define-shell-rule.js").ShellRule<[], NoSourceMessageId>;
|