@eslint-sh/eslint-plugin 0.5.0 → 0.6.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 +223 -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 +50 -0
- package/dist/configs/posix-portable.d.ts +40 -0
- package/dist/configs/posix-portable.js +52 -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-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 +197 -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 +88 -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 +289 -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 +347 -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 +78 -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-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 +128 -0
- package/docs/rules/shebang-matches-dialect.md +120 -0
- package/docs/selectors.md +2 -2
- package/docs/shellcheck.md +86 -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,33 @@
|
|
|
1
|
+
import type { DeclarationConvention } from './declaration-convention.js';
|
|
2
|
+
/**
|
|
3
|
+
* The reference {@link DeclarationConvention}: a script declares the external
|
|
4
|
+
* commands it uses in `# uses:` comments in its header.
|
|
5
|
+
*
|
|
6
|
+
* ```sh
|
|
7
|
+
* #!/usr/bin/env bash
|
|
8
|
+
* # uses: jq curl
|
|
9
|
+
* # uses: git
|
|
10
|
+
* set -euo pipefail
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* declares `jq`, `curl`, and `git`. The header is the run of blank and
|
|
14
|
+
* comment-only lines at the top of the file; a `# uses:` comment after the
|
|
15
|
+
* first command is not a declaration. Several pragma lines accumulate, names
|
|
16
|
+
* are whitespace-separated, and each declaration is `kind: 'script'` with a
|
|
17
|
+
* `range` that covers exactly that name. A word starting with `#` ends the list and begins a trailing
|
|
18
|
+
* remark: `# uses: jq # for json` declares only `jq`.
|
|
19
|
+
*
|
|
20
|
+
* Its `removeDeclaration` hook deletes one name and the whitespace that
|
|
21
|
+
* separated it, or the whole pragma line (with its `\n` or `\r\n`) when that
|
|
22
|
+
* name was the line's last.
|
|
23
|
+
*
|
|
24
|
+
* Its `declarationHint` hook tells a person exactly what to add: the line
|
|
25
|
+
* `# uses: curl`, on a line below the shebang, or as the first line of the
|
|
26
|
+
* file when there is no shebang. A name a pragma cannot express (one with
|
|
27
|
+
* whitespace, or starting with `#`) gets no hint.
|
|
28
|
+
*
|
|
29
|
+
* Bind it with `settings: { sh: { declarationConvention: commentPragma } }`.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export declare const commentPragma: Readonly<Required<DeclarationConvention>>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A header line (terminator excluded) that is blank or holds only a `#`
|
|
3
|
+
* comment. `[\s\S]` rather than `.`, so a stray `\r` or U+2028 inside a comment
|
|
4
|
+
* does not end the header early.
|
|
5
|
+
*/
|
|
6
|
+
const HEADER_LINE = /^[ \t]*(?:#[\s\S]*)?$/u;
|
|
7
|
+
/**
|
|
8
|
+
* A pragma line; group 1 is everything after `uses:`: the whitespace-separated
|
|
9
|
+
* name list, then optionally a `#`-word and a trailing remark.
|
|
10
|
+
*/
|
|
11
|
+
const PRAGMA_LINE = /^[ \t]*#[ \t]*uses:([\s\S]*)$/u;
|
|
12
|
+
/** One name in a pragma's name list. */
|
|
13
|
+
const PRAGMA_NAME = /\S+/gu;
|
|
14
|
+
/**
|
|
15
|
+
* Scans the header of `text` and returns its pragma lines in source order.
|
|
16
|
+
* Lines end at `\n`; a `\r` immediately before it belongs to the terminator
|
|
17
|
+
* (so CRLF files declare `jq`, never `jq\r`).
|
|
18
|
+
*/
|
|
19
|
+
function readPragmaLines(text) {
|
|
20
|
+
const pragmas = [];
|
|
21
|
+
let lineStart = 0;
|
|
22
|
+
while (lineStart < text.length) {
|
|
23
|
+
const newline = text.indexOf('\n', lineStart);
|
|
24
|
+
const lineEnd = newline === -1 ? text.length : newline + 1;
|
|
25
|
+
const contentEnd = newline === -1
|
|
26
|
+
? text.length
|
|
27
|
+
: newline > lineStart && text[newline - 1] === '\r'
|
|
28
|
+
? newline - 1
|
|
29
|
+
: newline;
|
|
30
|
+
const content = text.slice(lineStart, contentEnd);
|
|
31
|
+
if (!HEADER_LINE.test(content)) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
const pragma = PRAGMA_LINE.exec(content);
|
|
35
|
+
if (pragma) {
|
|
36
|
+
const list = pragma[1];
|
|
37
|
+
const listStart = lineStart + content.length - list.length;
|
|
38
|
+
const names = [];
|
|
39
|
+
for (const match of list.matchAll(PRAGMA_NAME)) {
|
|
40
|
+
// A `#`-word starts a trailing remark (`# uses: jq # for json`), not a name.
|
|
41
|
+
if (match[0].startsWith('#'))
|
|
42
|
+
break;
|
|
43
|
+
names.push({
|
|
44
|
+
kind: 'script',
|
|
45
|
+
name: match[0],
|
|
46
|
+
range: [listStart + match.index, listStart + match.index + match[0].length],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
pragmas.push({ lineStart, contentEnd, lineEnd, names });
|
|
50
|
+
}
|
|
51
|
+
lineStart = lineEnd;
|
|
52
|
+
}
|
|
53
|
+
return pragmas;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The `[start, end)` range that removes `names[index]` from its pragma line
|
|
57
|
+
* while the line keeps other names: the name plus the whitespace separating it
|
|
58
|
+
* from its successor, or, for the last name, from its predecessor. Either way
|
|
59
|
+
* the remaining names keep single, original separators. Only called with
|
|
60
|
+
* `names.length > 1`; a line's only name is removed with its whole line.
|
|
61
|
+
*/
|
|
62
|
+
function nameRemovalRange(names, index) {
|
|
63
|
+
const target = names[index];
|
|
64
|
+
return index < names.length - 1
|
|
65
|
+
? [target.range[0], names[index + 1].range[0]]
|
|
66
|
+
: [names[index - 1].range[1], target.range[1]];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The `[start, end)` range that deletes `line` entirely, terminator included.
|
|
70
|
+
* The header's last line can lack a terminator (a file that is nothing but
|
|
71
|
+
* header); there the newline *before* the line goes instead, so the file does
|
|
72
|
+
* not end in a newly empty line.
|
|
73
|
+
*/
|
|
74
|
+
function lineRemovalRange(text, line) {
|
|
75
|
+
if (line.lineEnd > line.contentEnd || line.lineStart === 0) {
|
|
76
|
+
return [line.lineStart, line.lineEnd];
|
|
77
|
+
}
|
|
78
|
+
const precedingTerminatorLength = text[line.lineStart - 2] === '\r' ? 2 : 1;
|
|
79
|
+
return [line.lineStart - precedingTerminatorLength, line.lineEnd];
|
|
80
|
+
}
|
|
81
|
+
function declaredCommands(sourceCode) {
|
|
82
|
+
return readPragmaLines(sourceCode.text).flatMap((line) => line.names);
|
|
83
|
+
}
|
|
84
|
+
function removeDeclaration(sourceCode, declared, fixer) {
|
|
85
|
+
const text = sourceCode.text;
|
|
86
|
+
for (const line of readPragmaLines(text)) {
|
|
87
|
+
const index = line.names.findIndex((candidate) => candidate.name === declared.name &&
|
|
88
|
+
candidate.range[0] === declared.range[0] &&
|
|
89
|
+
candidate.range[1] === declared.range[1]);
|
|
90
|
+
if (index === -1) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
return fixer.removeRange(line.names.length === 1 ? lineRemovalRange(text, line) : nameRemovalRange(line.names, index));
|
|
94
|
+
}
|
|
95
|
+
// Not one of this file's pragma declarations (for example, produced by a
|
|
96
|
+
// different convention): there is nothing this convention can safely edit.
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A name a pragma line can declare: one whitespace-free word that does not
|
|
101
|
+
* start a trailing remark (`#`).
|
|
102
|
+
*/
|
|
103
|
+
const DECLARABLE_NAME = /^[^\s#]\S*$/u;
|
|
104
|
+
/**
|
|
105
|
+
* Where to write a new pragma line so that it is in the header: directly
|
|
106
|
+
* below the shebang when the file has one (above it, the shebang would stop
|
|
107
|
+
* being line 1), otherwise as the first line. Either place is always a header
|
|
108
|
+
* line, whatever the rest of the header holds.
|
|
109
|
+
*/
|
|
110
|
+
function declarationHint(command, sourceCode) {
|
|
111
|
+
if (!DECLARABLE_NAME.test(command))
|
|
112
|
+
return null;
|
|
113
|
+
const where = sourceCode.text.startsWith('#!')
|
|
114
|
+
? 'on a line below the shebang'
|
|
115
|
+
: 'as the first line of the file';
|
|
116
|
+
return `Add \`# uses: ${command}\` ${where}.`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The reference {@link DeclarationConvention}: a script declares the external
|
|
120
|
+
* commands it uses in `# uses:` comments in its header.
|
|
121
|
+
*
|
|
122
|
+
* ```sh
|
|
123
|
+
* #!/usr/bin/env bash
|
|
124
|
+
* # uses: jq curl
|
|
125
|
+
* # uses: git
|
|
126
|
+
* set -euo pipefail
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* declares `jq`, `curl`, and `git`. The header is the run of blank and
|
|
130
|
+
* comment-only lines at the top of the file; a `# uses:` comment after the
|
|
131
|
+
* first command is not a declaration. Several pragma lines accumulate, names
|
|
132
|
+
* are whitespace-separated, and each declaration is `kind: 'script'` with a
|
|
133
|
+
* `range` that covers exactly that name. A word starting with `#` ends the list and begins a trailing
|
|
134
|
+
* remark: `# uses: jq # for json` declares only `jq`.
|
|
135
|
+
*
|
|
136
|
+
* Its `removeDeclaration` hook deletes one name and the whitespace that
|
|
137
|
+
* separated it, or the whole pragma line (with its `\n` or `\r\n`) when that
|
|
138
|
+
* name was the line's last.
|
|
139
|
+
*
|
|
140
|
+
* Its `declarationHint` hook tells a person exactly what to add: the line
|
|
141
|
+
* `# uses: curl`, on a line below the shebang, or as the first line of the
|
|
142
|
+
* file when there is no shebang. A name a pragma cannot express (one with
|
|
143
|
+
* whitespace, or starting with `#`) gets no hint.
|
|
144
|
+
*
|
|
145
|
+
* Bind it with `settings: { sh: { declarationConvention: commentPragma } }`.
|
|
146
|
+
*
|
|
147
|
+
* @public
|
|
148
|
+
*/
|
|
149
|
+
export const commentPragma = Object.freeze({
|
|
150
|
+
declaredCommands,
|
|
151
|
+
removeDeclaration,
|
|
152
|
+
declarationHint,
|
|
153
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The declaration-convention extension point: how a project states which
|
|
3
|
+
* external commands a script depends on.
|
|
4
|
+
*
|
|
5
|
+
* Which commands a script *uses* is a fact this plugin can derive from the
|
|
6
|
+
* syntax tree. How a script *declares* the commands it intends to use is
|
|
7
|
+
* project policy — a header comment, a manifest variable, a sourced
|
|
8
|
+
* dependency file — and differs between consumers. This plugin therefore
|
|
9
|
+
* never hardcodes a declaration syntax. Rules that compare usage against
|
|
10
|
+
* declarations ask a {@link DeclarationConvention} for the declarations and,
|
|
11
|
+
* when they offer an autofix, ask the same convention to produce the edit,
|
|
12
|
+
* because only the convention knows its own syntax.
|
|
13
|
+
*
|
|
14
|
+
* A convention is bound by the consumer, through the shared
|
|
15
|
+
* `settings.sh.declarationConvention` setting (see
|
|
16
|
+
* `../rules/resolve-declaration-convention.ts`). This package ships exactly one
|
|
17
|
+
* reference implementation, `commentPragma` (`./comment-pragma.ts`).
|
|
18
|
+
*/
|
|
19
|
+
import type { RuleTextEdit, RuleTextEditor } from '@eslint/core';
|
|
20
|
+
import type { ShellSourceCode } from '../language/shell-source-code.js';
|
|
21
|
+
/**
|
|
22
|
+
* The file ESLint is linting, as its rule context names it. A
|
|
23
|
+
* {@link DeclarationConvention} receives it so it can find declarations kept
|
|
24
|
+
* outside the script, such as a per-script sidecar file next to it.
|
|
25
|
+
*
|
|
26
|
+
* A rule's own `context` is a `LintedFile`, so a rule that calls
|
|
27
|
+
* `declaredCommands` passes `context`.
|
|
28
|
+
*
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export interface LintedFile {
|
|
32
|
+
/**
|
|
33
|
+
* The name ESLint lints the code under (`context.filename`). For a code
|
|
34
|
+
* block a processor extracts from another file it is virtual
|
|
35
|
+
* (`README.md/0_0.sh`).
|
|
36
|
+
*/
|
|
37
|
+
readonly filename: string;
|
|
38
|
+
/**
|
|
39
|
+
* The path on disk ESLint associates with the code
|
|
40
|
+
* (`context.physicalFilename`): the file it read, or the file a
|
|
41
|
+
* processor's code block came from. With `--stdin --stdin-filename`, it is
|
|
42
|
+
* the name given on the command line; the code came from standard input,
|
|
43
|
+
* and that file may not exist. Resolve sidecar paths from this one, and
|
|
44
|
+
* treat a missing sidecar as a normal case.
|
|
45
|
+
*/
|
|
46
|
+
readonly physicalFilename: string;
|
|
47
|
+
/**
|
|
48
|
+
* ESLint's working directory (`context.cwd`). A relative
|
|
49
|
+
* {@link FileDeclaredCommand.path} is relative to it.
|
|
50
|
+
*/
|
|
51
|
+
readonly cwd: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A declaration written in the linted script's own text: the only kind an
|
|
55
|
+
* ESLint fix can edit, because a fix edits only the file being linted.
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
export interface ScriptDeclaredCommand {
|
|
60
|
+
/** Says the declaration is in the script. */
|
|
61
|
+
readonly kind: 'script';
|
|
62
|
+
/** The declared command name, exactly as it would be invoked (`jq`, `docker-compose`). */
|
|
63
|
+
readonly name: string;
|
|
64
|
+
/**
|
|
65
|
+
* The `[start, end)` source range of this declaration, as UTF-16 offsets
|
|
66
|
+
* into `sourceCode.text` (the same coordinates as every node's `range`),
|
|
67
|
+
* with `0 <= start <= end <= sourceCode.text.length`. Rules report a
|
|
68
|
+
* declaration at this range, so it should cover the text that declares
|
|
69
|
+
* this one name and nothing else.
|
|
70
|
+
*/
|
|
71
|
+
readonly range: readonly [number, number];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A declaration written in a file other than the linted script: a per-script
|
|
75
|
+
* sidecar, or a manifest several scripts share. A report about it names the
|
|
76
|
+
* file, and carries no fix, since a fix can only edit the linted script.
|
|
77
|
+
*
|
|
78
|
+
* @public
|
|
79
|
+
*/
|
|
80
|
+
export interface FileDeclaredCommand {
|
|
81
|
+
/** Says the declaration is in another file. */
|
|
82
|
+
readonly kind: 'file';
|
|
83
|
+
/** The declared command name, exactly as it would be invoked. */
|
|
84
|
+
readonly name: string;
|
|
85
|
+
/**
|
|
86
|
+
* Where a person or a tool should look for the declaration: a path
|
|
87
|
+
* relative to {@link LintedFile.cwd}, or an absolute path. It must be a
|
|
88
|
+
* non-empty single line. Rules name it in their reports as given; they never
|
|
89
|
+
* read it.
|
|
90
|
+
*/
|
|
91
|
+
readonly path: string;
|
|
92
|
+
/** The 1-based line of `path` that declares the command, when the convention knows it. */
|
|
93
|
+
readonly line?: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A declaration whose source can only be described in words: there is no
|
|
97
|
+
* range in the script and no path to point at, only a phrase that tells a
|
|
98
|
+
* person where it comes from, such as a list written in the ESLint config, a
|
|
99
|
+
* convention's own constant, or a snapshot of a tool registry. A report about
|
|
100
|
+
* it names the source by `description` and carries no fix.
|
|
101
|
+
*
|
|
102
|
+
* @public
|
|
103
|
+
*/
|
|
104
|
+
export interface DescribedDeclaredCommand {
|
|
105
|
+
/** Says the declaration's source is described in words, by `description`. */
|
|
106
|
+
readonly kind: 'described';
|
|
107
|
+
/** The declared command name, exactly as it would be invoked. */
|
|
108
|
+
readonly name: string;
|
|
109
|
+
/**
|
|
110
|
+
* Names the source for a person who wants to change the declaration, as a
|
|
111
|
+
* phrase that reads after "declared in": "the web image list in
|
|
112
|
+
* eslint.config.js", "the tool registry snapshot of 2026-11-01". It must be a
|
|
113
|
+
* non-empty single line.
|
|
114
|
+
*/
|
|
115
|
+
readonly description: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* One command a script declares it depends on, as reported by a
|
|
119
|
+
* {@link DeclarationConvention}. Where the declaration lives is part of what
|
|
120
|
+
* it is, so it is one of three kinds, told apart by the required `kind`:
|
|
121
|
+
* written in the script ({@link ScriptDeclaredCommand}), in another file
|
|
122
|
+
* ({@link FileDeclaredCommand}), or in a source that can only be described in
|
|
123
|
+
* words ({@link DescribedDeclaredCommand}). Only the script kind has a `range`, so
|
|
124
|
+
* narrow on `kind` before reading it.
|
|
125
|
+
*
|
|
126
|
+
* @public
|
|
127
|
+
*/
|
|
128
|
+
export type DeclaredCommand = ScriptDeclaredCommand | FileDeclaredCommand | DescribedDeclaredCommand;
|
|
129
|
+
/**
|
|
130
|
+
* How a project declares the external commands its shell scripts depend on.
|
|
131
|
+
*
|
|
132
|
+
* Implement this to teach the plugin a project's own declaration syntax, then
|
|
133
|
+
* bind it with `settings: { sh: { declarationConvention: myConvention } }`.
|
|
134
|
+
* Until a convention is bound, rules that consume one report nothing.
|
|
135
|
+
*
|
|
136
|
+
* Bind it through `settings`, not through a rule's options: ESLint copies
|
|
137
|
+
* rule options with `structuredClone`, which cannot copy functions. Define it
|
|
138
|
+
* as a plain object whose methods are its own properties, and bind it in one
|
|
139
|
+
* config entry: when two matching config entries both set it, ESLint merges
|
|
140
|
+
* the two objects property by property, which drops methods inherited from a
|
|
141
|
+
* class prototype and can mix methods from two different conventions.
|
|
142
|
+
*
|
|
143
|
+
* One convention object serves every file its config entry matches, so its
|
|
144
|
+
* answer must depend only on its arguments and on data it holds, never on
|
|
145
|
+
* which file it saw before. ESLint's `--cache` sees a convention's own
|
|
146
|
+
* enumerable data properties (they are part of the config it hashes) but not
|
|
147
|
+
* files the convention reads while linting.
|
|
148
|
+
*
|
|
149
|
+
* {@link commentPragma} is the reference implementation.
|
|
150
|
+
*
|
|
151
|
+
* @public
|
|
152
|
+
*/
|
|
153
|
+
export interface DeclarationConvention {
|
|
154
|
+
/**
|
|
155
|
+
* Returns every command the linted file declares, in source order. A name
|
|
156
|
+
* declared twice appears twice, each with its own source. `file` names the
|
|
157
|
+
* file being linted, for a convention whose declarations live beside it;
|
|
158
|
+
* a convention that reads only `sourceCode` can leave it out.
|
|
159
|
+
*/
|
|
160
|
+
declaredCommands(sourceCode: ShellSourceCode, file: LintedFile): DeclaredCommand[];
|
|
161
|
+
/**
|
|
162
|
+
* Optional edit hook: returns the fix that deletes the single stale
|
|
163
|
+
* declaration `declared` (an in-script entry previously returned by
|
|
164
|
+
* {@link DeclarationConvention.declaredCommands} for the same
|
|
165
|
+
* `sourceCode`), built with the rule's `fixer`. Returns `null` when that
|
|
166
|
+
* declaration cannot be removed safely.
|
|
167
|
+
*
|
|
168
|
+
* It is only ever called with a {@link ScriptDeclaredCommand}: a fix edits
|
|
169
|
+
* only the linted script, and no edit to the script removes a declaration
|
|
170
|
+
* kept anywhere else.
|
|
171
|
+
*
|
|
172
|
+
* A convention without this hook still reports stale declarations; those
|
|
173
|
+
* reports simply carry no autofix.
|
|
174
|
+
*/
|
|
175
|
+
removeDeclaration?(sourceCode: ShellSourceCode, declared: ScriptDeclaredCommand, fixer: RuleTextEditor): RuleTextEdit | RuleTextEdit[] | null;
|
|
176
|
+
/**
|
|
177
|
+
* Optional hint hook: returns one short sentence that tells a person how to
|
|
178
|
+
* declare `command`, with text they can copy. {@link commentPragma}'s hint says to
|
|
179
|
+
* add `# uses: curl` on a line below the shebang.
|
|
180
|
+
* `sh/no-undeclared-commands` appends it to its report. Following the
|
|
181
|
+
* sentence literally must make that report go away.
|
|
182
|
+
*
|
|
183
|
+
* Return `null` when this convention cannot declare `command` as written
|
|
184
|
+
* (for example, a name its syntax cannot express). The report then falls
|
|
185
|
+
* back to a generic sentence, as it does for a convention without this
|
|
186
|
+
* hook. Anything other than `null` must be a non-empty single-line string.
|
|
187
|
+
*/
|
|
188
|
+
declarationHint?(command: string, sourceCode: ShellSourceCode, file: LintedFile): string | null;
|
|
189
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one path by which a rule reads a bound convention's declarations for
|
|
3
|
+
* the file it is linting. The built-in consumers (`sh/no-undeclared-commands`,
|
|
4
|
+
* `sh/no-unused-declarations`) and a pack's own rules all call
|
|
5
|
+
* {@link readDeclaredCommands}, so they hand the convention the same
|
|
6
|
+
* {@link LintedFile} and trust only declarations that passed the same checks.
|
|
7
|
+
*
|
|
8
|
+
* The checks exist because a declaration's kind decides where a report goes
|
|
9
|
+
* and whether a fix is offered. A declaration of an unknown kind, one missing
|
|
10
|
+
* its kind's fields, or a script range outside the text would otherwise be
|
|
11
|
+
* reported at a made-up location, crash deep inside a rule, or be silently
|
|
12
|
+
* accepted by a rule that only reads names. Failing with the declaration and
|
|
13
|
+
* the setting in the message makes the convention's bug the thing the user
|
|
14
|
+
* sees.
|
|
15
|
+
*/
|
|
16
|
+
import type { ShellSourceCode } from '../language/shell-source-code.js';
|
|
17
|
+
import type { DeclarationConvention, DeclaredCommand, LintedFile } from './declaration-convention.js';
|
|
18
|
+
/**
|
|
19
|
+
* What {@link readDeclaredCommands} reads from a rule's context: the linted
|
|
20
|
+
* file and its source code. Any shell rule's `context` satisfies it.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export interface DeclarationReadContext extends LintedFile {
|
|
25
|
+
/** The source code of the file being linted. */
|
|
26
|
+
readonly sourceCode: ShellSourceCode;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The only view of the linted file a convention's hooks receive: `filename`,
|
|
30
|
+
* `physicalFilename` and `cwd`, frozen. A rule's `context` must never reach a
|
|
31
|
+
* hook, because it would let a convention report, read options or reach other
|
|
32
|
+
* rule state.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export declare function toLintedFile(context: LintedFile): LintedFile;
|
|
37
|
+
/**
|
|
38
|
+
* Returns the declarations `convention` makes for the file `context` is
|
|
39
|
+
* linting, after checking each one against its kind. Call it from a rule's
|
|
40
|
+
* listener with the convention `resolveDeclarationConvention` returned and
|
|
41
|
+
* the rule's own `context`.
|
|
42
|
+
*
|
|
43
|
+
* The convention receives a frozen {@link LintedFile} holding only
|
|
44
|
+
* `filename`, `physicalFilename` and `cwd`, never the whole context, so it
|
|
45
|
+
* sees which file it is reading and nothing it could report through.
|
|
46
|
+
*
|
|
47
|
+
* Throws an `Error` that names the setting, starting
|
|
48
|
+
* `Invalid settings.sh.declarationConvention:`, and shows the offending value when
|
|
49
|
+
* `declaredCommands` does not return an array, or returns a declaration with
|
|
50
|
+
* a missing or unknown `kind`, without its kind's fields, or with a `range`
|
|
51
|
+
* outside the linted text.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const convention = resolveDeclarationConvention(context);
|
|
56
|
+
* if (convention === null) return {};
|
|
57
|
+
* return {
|
|
58
|
+
* File() {
|
|
59
|
+
* for (const declared of readDeclaredCommands(convention, context)) {
|
|
60
|
+
* if (declared.kind === 'script') {
|
|
61
|
+
* // declared.range is an offset pair into context.sourceCode.text
|
|
62
|
+
* }
|
|
63
|
+
* }
|
|
64
|
+
* },
|
|
65
|
+
* };
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
export declare function readDeclaredCommands(convention: DeclarationConvention, context: DeclarationReadContext): DeclaredCommand[];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The binding a validation error names. A convention can only be bound in
|
|
3
|
+
* settings in practice: ESLint copies rule options with `structuredClone`,
|
|
4
|
+
* which rejects an object with methods.
|
|
5
|
+
*/
|
|
6
|
+
const BINDING = 'settings.sh.declarationConvention';
|
|
7
|
+
/**
|
|
8
|
+
* The only view of the linted file a convention's hooks receive: `filename`,
|
|
9
|
+
* `physicalFilename` and `cwd`, frozen. A rule's `context` must never reach a
|
|
10
|
+
* hook, because it would let a convention report, read options or reach other
|
|
11
|
+
* rule state.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export function toLintedFile(context) {
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
filename: context.filename,
|
|
18
|
+
physicalFilename: context.physicalFilename,
|
|
19
|
+
cwd: context.cwd,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** `value` is a non-empty string with no line break or NUL, fit to name a source in one message line. */
|
|
23
|
+
function isSingleLine(value) {
|
|
24
|
+
return typeof value === 'string' && value !== '' && !/[\r\n\0]/u.test(value);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Why `value` is not a valid {@link DeclaredCommand} for a file whose text is
|
|
28
|
+
* `textLength` UTF-16 units long, or `null` when it is. Each kind is held to
|
|
29
|
+
* exactly the fields its reports need.
|
|
30
|
+
*/
|
|
31
|
+
function invalidReason(value, textLength) {
|
|
32
|
+
if (typeof value !== 'object' || value === null)
|
|
33
|
+
return 'it is not an object';
|
|
34
|
+
const declared = value;
|
|
35
|
+
if (typeof declared.name !== 'string' || declared.name === '') {
|
|
36
|
+
return '`name` must be a non-empty string';
|
|
37
|
+
}
|
|
38
|
+
switch (declared.kind) {
|
|
39
|
+
case 'script': {
|
|
40
|
+
const { range } = declared;
|
|
41
|
+
const valid = Array.isArray(range) &&
|
|
42
|
+
range.length === 2 &&
|
|
43
|
+
Number.isInteger(range[0]) &&
|
|
44
|
+
Number.isInteger(range[1]) &&
|
|
45
|
+
0 <= range[0] &&
|
|
46
|
+
range[0] <= range[1] &&
|
|
47
|
+
range[1] <= textLength;
|
|
48
|
+
return valid
|
|
49
|
+
? null
|
|
50
|
+
: `a script declaration needs \`range\`: two offsets [start, end) with 0 <= start <= end <= ${String(textLength)}, the length of the linted text`;
|
|
51
|
+
}
|
|
52
|
+
case 'file':
|
|
53
|
+
if (!isSingleLine(declared.path)) {
|
|
54
|
+
return 'a file declaration needs `path`: a non-empty single-line path, relative to `cwd` or absolute';
|
|
55
|
+
}
|
|
56
|
+
return declared.line === undefined ||
|
|
57
|
+
(Number.isInteger(declared.line) && declared.line >= 1)
|
|
58
|
+
? null
|
|
59
|
+
: 'the optional `line` of a file declaration must be a positive integer';
|
|
60
|
+
case 'described':
|
|
61
|
+
return isSingleLine(declared.description)
|
|
62
|
+
? null
|
|
63
|
+
: 'a described declaration needs `description`: a non-empty single line';
|
|
64
|
+
case undefined:
|
|
65
|
+
return "`kind` is missing: it must be 'script', 'file' or 'described'";
|
|
66
|
+
default:
|
|
67
|
+
return `\`kind\` must be 'script', 'file' or 'described', not ${describe(declared.kind)}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** A short rendering of a value for an error message. */
|
|
71
|
+
function describe(value) {
|
|
72
|
+
try {
|
|
73
|
+
// `JSON.stringify` returns `undefined` for a function or `undefined`.
|
|
74
|
+
const text = JSON.stringify(value);
|
|
75
|
+
return text ?? String(value);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return String(value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Returns the declarations `convention` makes for the file `context` is
|
|
83
|
+
* linting, after checking each one against its kind. Call it from a rule's
|
|
84
|
+
* listener with the convention `resolveDeclarationConvention` returned and
|
|
85
|
+
* the rule's own `context`.
|
|
86
|
+
*
|
|
87
|
+
* The convention receives a frozen {@link LintedFile} holding only
|
|
88
|
+
* `filename`, `physicalFilename` and `cwd`, never the whole context, so it
|
|
89
|
+
* sees which file it is reading and nothing it could report through.
|
|
90
|
+
*
|
|
91
|
+
* Throws an `Error` that names the setting, starting
|
|
92
|
+
* `Invalid settings.sh.declarationConvention:`, and shows the offending value when
|
|
93
|
+
* `declaredCommands` does not return an array, or returns a declaration with
|
|
94
|
+
* a missing or unknown `kind`, without its kind's fields, or with a `range`
|
|
95
|
+
* outside the linted text.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* const convention = resolveDeclarationConvention(context);
|
|
100
|
+
* if (convention === null) return {};
|
|
101
|
+
* return {
|
|
102
|
+
* File() {
|
|
103
|
+
* for (const declared of readDeclaredCommands(convention, context)) {
|
|
104
|
+
* if (declared.kind === 'script') {
|
|
105
|
+
* // declared.range is an offset pair into context.sourceCode.text
|
|
106
|
+
* }
|
|
107
|
+
* }
|
|
108
|
+
* },
|
|
109
|
+
* };
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* @public
|
|
113
|
+
*/
|
|
114
|
+
export function readDeclaredCommands(convention, context) {
|
|
115
|
+
const { sourceCode } = context;
|
|
116
|
+
const file = toLintedFile(context);
|
|
117
|
+
const declarations = convention.declaredCommands(sourceCode, file);
|
|
118
|
+
if (!Array.isArray(declarations)) {
|
|
119
|
+
throw new Error(`Invalid ${BINDING}: \`declaredCommands\` must return an array, but returned ${describe(declarations)}.`);
|
|
120
|
+
}
|
|
121
|
+
for (const declared of declarations) {
|
|
122
|
+
const reason = invalidReason(declared, sourceCode.text.length);
|
|
123
|
+
if (reason !== null) {
|
|
124
|
+
throw new Error(`Invalid ${BINDING}: \`declaredCommands\` returned the declaration ${describe(declared)}, but ${reason}.`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return declarations;
|
|
128
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The faithful shell analysis surface (`@eslint-sh/eslint-plugin/syntax/analyze`): the complete
|
|
3
|
+
* public surface of `sh-ast/analyze`, re-exported unchanged from the very `sh-ast` instance this
|
|
4
|
+
* plugin's language parses with.
|
|
5
|
+
*
|
|
6
|
+
* It provides `enumerateCommands`, `resolveArgv0` (whose `chainIndices` locate a resolved command's
|
|
7
|
+
* arguments in its site's `argv`), `DEFAULT_TRANSPARENT_WRAPPERS`, `WrapperSpec`, the raw
|
|
8
|
+
* `resolveWord(Node)`, and the named operator constants with `rawOperator` for comparing and
|
|
9
|
+
* constructing the operator values those records carry. All of it works on the faithful records
|
|
10
|
+
* from `@eslint-sh/eslint-plugin/syntax`. The plugin root's `resolveWord` takes a lint-view
|
|
11
|
+
* `ShellNode` instead; the two share a name but not an input contract, so this subpath is kept
|
|
12
|
+
* separate from the root.
|
|
13
|
+
*
|
|
14
|
+
* Like `@eslint-sh/eslint-plugin/syntax`, it tracks the plugin's pinned `sh-ast` minor and never
|
|
15
|
+
* wraps or renames anything.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export * from "sh-ast/analyze";
|
|
22
|
+
|
|
23
|
+
export { }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The faithful shell syntax surface (`@eslint-sh/eslint-plugin/syntax`): the complete public
|
|
3
|
+
* surface of `sh-ast`'s root entry point, re-exported unchanged from the very `sh-ast` instance
|
|
4
|
+
* this plugin's language parses with.
|
|
5
|
+
*
|
|
6
|
+
* Use it when a custom rule edits or analyzes the parser's own records rather than the plugin's
|
|
7
|
+
* lint view: `walk`, `parseSync`, `printSync` and `PrintOptions`, the record types (`Node`,
|
|
8
|
+
* `Word`, `CallExpr`, `SyntaxPos`, `GoString`, …), and the operator and language constants.
|
|
9
|
+
* Importing them from here, instead of installing `sh-ast` separately, means a rule always gets
|
|
10
|
+
* the same `sh-ast` version and module instance as the language that produced its syntax. Under
|
|
11
|
+
* 0.x a different `sh-ast` minor is a breaking change, and records from one copy are not
|
|
12
|
+
* guaranteed to be accepted by another.
|
|
13
|
+
*
|
|
14
|
+
* This entry point tracks the plugin's pinned `sh-ast` minor, so an `sh-ast` 0.x minor bump is a
|
|
15
|
+
* minor release of this plugin. It never wraps or renames anything: every name has `sh-ast`'s
|
|
16
|
+
* own contract. The plugin root (`@eslint-sh/eslint-plugin`) keeps the view-aware helpers such
|
|
17
|
+
* as `resolveWord(ShellNode)`, which differ from the raw helpers of the same name in
|
|
18
|
+
* `@eslint-sh/eslint-plugin/syntax/analyze`. That is why these names live on their own subpath
|
|
19
|
+
* and are not merged into the root.
|
|
20
|
+
*
|
|
21
|
+
* @packageDocumentation
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export * from "sh-ast";
|
|
26
|
+
|
|
27
|
+
export { }
|