@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
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { ConfigCommentParser, Directive, TextSourceCodeBase, VisitNodeStep, } from '@eslint/plugin-kit';
|
|
2
|
-
import { isShellComment,
|
|
2
|
+
import { childNodes, isShellComment, projectedComments, projectedCoordinates, syntaxForNode, viewForSyntax, walkShell, } from './syntax-view.js';
|
|
3
|
+
import { SourceCoordinates } from './source-coordinates.js';
|
|
4
|
+
import { SYNTAX_INTERFACES, printSync } from 'sh-ast';
|
|
5
|
+
import { resolveArgv0 } from 'sh-ast/analyze';
|
|
6
|
+
import { buildCommandSiteIndex, rawCallExprFor, viewCommandSites } from './command-resolution.js';
|
|
7
|
+
import { resolveWord } from './word-resolution.js';
|
|
3
8
|
/**
|
|
4
9
|
* Shared instance β `ConfigCommentParser` is stateless (see
|
|
5
10
|
* `@eslint/plugin-kit`'s implementation), so every `ShellSourceCode`
|
|
@@ -39,12 +44,9 @@ function buildDirectiveLabels(directivePrefix) {
|
|
|
39
44
|
* call: the leading `#` is already excluded, e.g. the source
|
|
40
45
|
* `# eslint-disable-next-line sh/foo -- reason` produces
|
|
41
46
|
* `comment.text === " eslint-disable-next-line sh/foo -- reason"`).
|
|
42
|
-
* `ShNode`'s index signature types every field `unknown`, so this narrows
|
|
43
|
-
* defensively rather than assuming the shape.
|
|
44
47
|
*/
|
|
45
48
|
function getCommentText(comment) {
|
|
46
|
-
|
|
47
|
-
return typeof text === 'string' ? text : '';
|
|
49
|
+
return comment.text;
|
|
48
50
|
}
|
|
49
51
|
/**
|
|
50
52
|
* Length of the line terminator starting at `text[index]` β `2` for `\r\n`,
|
|
@@ -179,6 +181,75 @@ function computeUnusedDirectiveFixRange(comment, text) {
|
|
|
179
181
|
}
|
|
180
182
|
return [start, end + newlineLength];
|
|
181
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Returns the byte offset of `pos` when it names an exact boundary the parser
|
|
186
|
+
* observed, and throws otherwise. Invalid, recovered, and overflowed positions
|
|
187
|
+
* carry no exact offset (upstream may report zero or a saturated value), so
|
|
188
|
+
* converting them would place a report or fix somewhere the parser never
|
|
189
|
+
* pointed. They are rejected rather than rounded to a nearby boundary.
|
|
190
|
+
*
|
|
191
|
+
* @throws TypeError when `pos` is not shaped like an sh-ast `SyntaxPos`.
|
|
192
|
+
* @throws RangeError when `pos` is invalid, recovered, or overflowed.
|
|
193
|
+
*/
|
|
194
|
+
function exactByteOffset(pos) {
|
|
195
|
+
const candidate = pos;
|
|
196
|
+
if (typeof candidate !== 'object' ||
|
|
197
|
+
candidate === null ||
|
|
198
|
+
typeof pos.Offset !== 'number' ||
|
|
199
|
+
typeof pos.IsValid !== 'boolean' ||
|
|
200
|
+
typeof pos.IsRecovered !== 'boolean' ||
|
|
201
|
+
typeof pos.OffsetOverflow !== 'boolean') {
|
|
202
|
+
throw new TypeError('expected an sh-ast SyntaxPos');
|
|
203
|
+
}
|
|
204
|
+
if (!pos.IsValid || pos.IsRecovered || pos.OffsetOverflow) {
|
|
205
|
+
throw new RangeError(`syntax position has no exact source offset (IsValid: ${String(pos.IsValid)}, ` +
|
|
206
|
+
`IsRecovered: ${String(pos.IsRecovered)}, OffsetOverflow: ${String(pos.OffsetOverflow)})`);
|
|
207
|
+
}
|
|
208
|
+
return pos.Offset;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The record types `printSync` accepts as a root: `File`, `Stmt`, `Word`,
|
|
212
|
+
* `Assign`, every `Command` and every `WordPart` (sh-ast's documented
|
|
213
|
+
* supported roots). Any other record makes upstream's printer fail, so
|
|
214
|
+
* {@link ShellSourceCode.getPrintedReplacement} rejects it up front with a
|
|
215
|
+
* `TypeError` that names the type instead of surfacing a printer error.
|
|
216
|
+
*/
|
|
217
|
+
const printableRootTypes = new Set([
|
|
218
|
+
'File',
|
|
219
|
+
'Stmt',
|
|
220
|
+
'Word',
|
|
221
|
+
'Assign',
|
|
222
|
+
...SYNTAX_INTERFACES.Command,
|
|
223
|
+
...SYNTAX_INTERFACES.WordPart,
|
|
224
|
+
]);
|
|
225
|
+
/**
|
|
226
|
+
* Returns `edited`'s record type when it is a printable root, and throws a
|
|
227
|
+
* `TypeError` naming the type otherwise. `edited` is checked as an unknown
|
|
228
|
+
* value because rule code can pass anything.
|
|
229
|
+
*/
|
|
230
|
+
function printableRootType(edited) {
|
|
231
|
+
const candidate = edited;
|
|
232
|
+
if (typeof candidate !== 'object' ||
|
|
233
|
+
candidate === null ||
|
|
234
|
+
!('Type' in candidate) ||
|
|
235
|
+
typeof candidate.Type !== 'string') {
|
|
236
|
+
throw new TypeError('expected an sh-ast syntax record to print');
|
|
237
|
+
}
|
|
238
|
+
if (!printableRootTypes.has(candidate.Type)) {
|
|
239
|
+
throw new TypeError(`cannot print a ${candidate.Type} as a fix: printSync accepts File, Stmt, Word, Assign, ` +
|
|
240
|
+
'Command and WordPart roots');
|
|
241
|
+
}
|
|
242
|
+
return candidate.Type;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Whether the printer writes `stmt`'s terminator itself. A background (`&`),
|
|
246
|
+
* coprocess (`|&`) or disowned statement prints its own operator; any other
|
|
247
|
+
* statement prints with no terminator at all, because upstream leaves `;` to
|
|
248
|
+
* the enclosing statement list.
|
|
249
|
+
*/
|
|
250
|
+
function printsOwnTerminator(stmt) {
|
|
251
|
+
return stmt.Background || stmt.Coprocess || stmt.Disown;
|
|
252
|
+
}
|
|
182
253
|
/**
|
|
183
254
|
* Wraps `comment` in a shallow copy whose `.range` is
|
|
184
255
|
* {@link computeUnusedDirectiveFixRange}'s (possibly widened) range, while
|
|
@@ -209,8 +280,8 @@ function withUnusedDirectiveFixRange(comment, text) {
|
|
|
209
280
|
* `applyInlineConfig`) recognizes all four disable-comment forms
|
|
210
281
|
* (`<prefix>-disable`, `<prefix>-enable`, `<prefix>-disable-line`,
|
|
211
282
|
* `<prefix>-disable-next-line`) plus whole-file inline rule config
|
|
212
|
-
* (bare `<prefix> <rule-config>`), sourced from `Comment` nodes
|
|
213
|
-
*
|
|
283
|
+
* (bare `<prefix> <rule-config>`), sourced from the tree's `Comment` nodes
|
|
284
|
+
* and parsed with
|
|
214
285
|
* `@eslint/plugin-kit`'s `ConfigCommentParser` β the same building blocks
|
|
215
286
|
* `@eslint/json`'s `JSONSourceCode` uses, generalized so the `<prefix>` is
|
|
216
287
|
* configurable (default `"eslint"`; see `ShellLanguageOptions.directivePrefix`)
|
|
@@ -220,6 +291,24 @@ function withUnusedDirectiveFixRange(comment, text) {
|
|
|
220
291
|
* classes) is implemented on `ShellLanguage` β see `shell-language.ts` and
|
|
221
292
|
* `selector-classes.ts`.
|
|
222
293
|
*
|
|
294
|
+
* Rule-author helpers (`resolveWordText`, `getCommandName`,
|
|
295
|
+
* `resolveInvokedCommand`, `getCommandSites`) answer "is this word statically
|
|
296
|
+
* the string `jq`?", "which command does this invocation run?", and "which
|
|
297
|
+
* commands does this file run, and where?" by delegating to
|
|
298
|
+
* `sh-ast/analyze`. They never reinterpret words themselves, so every rule
|
|
299
|
+
* gets the same answer for the same syntax. `dialect` and `shellVersion`
|
|
300
|
+
* expose the file's normalized language options.
|
|
301
|
+
*
|
|
302
|
+
* Syntax access (`getSyntax`, `getViewNode`, `getIndexFromSyntaxPos`,
|
|
303
|
+
* `getLocFromSyntaxPos`) reaches behind the lint view to the sh-ast records
|
|
304
|
+
* this file was parsed into: from a view node to its record and back, and
|
|
305
|
+
* from a record's byte positions (such as `BinaryCmd.OpPos`, which the view
|
|
306
|
+
* leaves out) to ESLint coordinates in this file's text.
|
|
307
|
+
*
|
|
308
|
+
* Printed fixes (`getEditableSyntax`, `getPrintedReplacement`) edit an
|
|
309
|
+
* independent copy of a view node's record and print it with sh-ast's
|
|
310
|
+
* printer, returning the source range and text for `fixer.replaceTextRange`.
|
|
311
|
+
*
|
|
223
312
|
* @public
|
|
224
313
|
*/
|
|
225
314
|
export class ShellSourceCode extends TextSourceCodeBase {
|
|
@@ -254,7 +343,7 @@ export class ShellSourceCode extends TextSourceCodeBase {
|
|
|
254
343
|
*/
|
|
255
344
|
#inlineConfigPattern;
|
|
256
345
|
/**
|
|
257
|
-
* Comments
|
|
346
|
+
* Comments in the consumer's child graph. Source-order sorting
|
|
258
347
|
* is required for repeated inline configuration; schema field order does not
|
|
259
348
|
* encode physical source order. The lint view is a stable parse snapshot.
|
|
260
349
|
*/
|
|
@@ -265,15 +354,460 @@ export class ShellSourceCode extends TextSourceCodeBase {
|
|
|
265
354
|
* config, once indirectly via `getDisableDirectives`/`applyInlineConfig`).
|
|
266
355
|
*/
|
|
267
356
|
#inlineConfigNodes;
|
|
357
|
+
/** The normalized `languageOptions.dialect` this file was parsed with. */
|
|
358
|
+
#dialect;
|
|
359
|
+
/** The configured `languageOptions.shellVersion`, if the config supplied one. */
|
|
360
|
+
#shellVersion;
|
|
361
|
+
/** This file's coordinate index; see `#textCoordinates`. */
|
|
362
|
+
#ownCoordinates;
|
|
363
|
+
/**
|
|
364
|
+
* Every command site in this file, from one `enumerateCommands` call over
|
|
365
|
+
* the original syntax, built the first time a command helper needs it and
|
|
366
|
+
* reused for the rest of the lint run. Files whose rules never ask a
|
|
367
|
+
* command question never pay for (or fail on) enumeration.
|
|
368
|
+
*/
|
|
369
|
+
#commandSites;
|
|
370
|
+
/**
|
|
371
|
+
* {@link ShellSourceCode.getCommandSites}'s answer: the index's sites in
|
|
372
|
+
* view terms, built once from `#commandSites` so every caller shares one
|
|
373
|
+
* frozen array.
|
|
374
|
+
*/
|
|
375
|
+
#commandSiteViews;
|
|
268
376
|
constructor(options) {
|
|
269
377
|
super(options);
|
|
270
|
-
// Defensive
|
|
378
|
+
// Defensive defaults for direct construction outside the `ShellLanguage`
|
|
271
379
|
// pipeline (e.g. unit tests) β `ShellLanguage.createSourceCode` always
|
|
272
|
-
// supplies the already-normalized, defaulted
|
|
380
|
+
// supplies the already-normalized, defaulted values.
|
|
273
381
|
this.#directivePrefix = options.directivePrefix ?? 'eslint';
|
|
382
|
+
this.#dialect = options.dialect ?? 'bash';
|
|
383
|
+
this.#shellVersion = options.shellVersion;
|
|
274
384
|
this.#directiveLabels = buildDirectiveLabels(this.#directivePrefix);
|
|
275
385
|
this.#inlineConfigPattern = buildInlineConfigPattern(this.#directivePrefix);
|
|
276
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* The shell dialect this file was parsed as: the normalized
|
|
389
|
+
* `languageOptions.dialect`, which defaults to `"bash"` when the config
|
|
390
|
+
* leaves it unset. Rules use it to gate dialect-specific checks.
|
|
391
|
+
*/
|
|
392
|
+
get dialect() {
|
|
393
|
+
return this.#dialect;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* The configured `languageOptions.shellVersion` (a `MAJOR[.MINOR]` string
|
|
397
|
+
* such as `"3.2"`), or `undefined` when the config does not set one. The
|
|
398
|
+
* parser never reads it; it exists for rules that gate on shell version.
|
|
399
|
+
*/
|
|
400
|
+
get shellVersion() {
|
|
401
|
+
return this.#shellVersion;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* The one-based location of a UTF-16 `index` in this file's text, where
|
|
405
|
+
* `index` may be anywhere from 0 to `text.length`. CRLF counts as one line
|
|
406
|
+
* break.
|
|
407
|
+
*
|
|
408
|
+
* Overrides the base class, which measures every index from the root
|
|
409
|
+
* node's own start and so assumes the root spans the whole text. A shell
|
|
410
|
+
* file's root starts at its first statement or comment, so the base answer
|
|
411
|
+
* was shifted by any leading blank lines or indentation.
|
|
412
|
+
*
|
|
413
|
+
* @throws TypeError when `index` is not a number.
|
|
414
|
+
* @throws RangeError when `index` is not an integer from 0 to `text.length`.
|
|
415
|
+
*/
|
|
416
|
+
getLocFromIndex(index) {
|
|
417
|
+
if (typeof index !== 'number')
|
|
418
|
+
throw new TypeError('Expected `index` to be a number.');
|
|
419
|
+
const { line, column } = this.#textCoordinates().atIndex(index);
|
|
420
|
+
return { line, column };
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The UTF-16 index of a one-based `loc` in this file's text: the inverse of
|
|
424
|
+
* {@link ShellSourceCode.getLocFromIndex}. The column may name any position
|
|
425
|
+
* on its line up to and including the line break.
|
|
426
|
+
*
|
|
427
|
+
* Overrides the base class for the same reason as `getLocFromIndex`.
|
|
428
|
+
*
|
|
429
|
+
* @throws TypeError when `loc` is not an object with numeric `line` and `column`.
|
|
430
|
+
* @throws RangeError when `loc` is not inside the text.
|
|
431
|
+
*/
|
|
432
|
+
getIndexFromLoc(loc) {
|
|
433
|
+
if (typeof loc !== 'object' ||
|
|
434
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- callers may be untyped JavaScript
|
|
435
|
+
loc === null ||
|
|
436
|
+
typeof loc.line !== 'number' ||
|
|
437
|
+
typeof loc.column !== 'number') {
|
|
438
|
+
throw new TypeError('Expected `loc` to be an object with numeric `line` and `column` properties.');
|
|
439
|
+
}
|
|
440
|
+
return this.#textCoordinates().indexAt(loc.line, loc.column);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* The coordinate index of this file's text: the one the language built while
|
|
444
|
+
* parsing, or, for a source code constructed directly, one built from the
|
|
445
|
+
* text on first use.
|
|
446
|
+
*/
|
|
447
|
+
#textCoordinates() {
|
|
448
|
+
this.#ownCoordinates ??= projectedCoordinates(this.ast) ?? new SourceCoordinates(this.text);
|
|
449
|
+
return this.#ownCoordinates;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Returns the literal text of `word` when `sh-ast/analyze`'s `resolveWord`
|
|
453
|
+
* reports it static, or `null` otherwise.
|
|
454
|
+
*
|
|
455
|
+
* Delegates to `resolveWord` (through this package's `resolveWord` export),
|
|
456
|
+
* which alone defines what "static" means: a word is static iff
|
|
457
|
+
* `resolveWord` reports `static: true`. Its non-static reasons are
|
|
458
|
+
* `expansion`, `tilde`, `glob`, `brace`, `locale`, and `unsupported`.
|
|
459
|
+
* Call the root `resolveWord` export instead when the reason matters.
|
|
460
|
+
*
|
|
461
|
+
* `options` is forwarded to `resolveWord` unchanged. When it is omitted, the
|
|
462
|
+
* word's grammar position picks the tilde context: a word in a `CallExpr`'s
|
|
463
|
+
* `args` uses `{ context: 'command-argument' }`, the context
|
|
464
|
+
* `enumerateCommands` uses for argv, so `resolveWordText(callExpr.args[0])`
|
|
465
|
+
* equals `getCommandName(callExpr)`. Any other word uses `resolveWord`'s
|
|
466
|
+
* conservative default, which also treats a `~` after an unquoted `:` as a
|
|
467
|
+
* tilde expansion, as assignment values do.
|
|
468
|
+
*
|
|
469
|
+
* @param word - A parsed `Word` node from this file's lint tree.
|
|
470
|
+
* @param options - Forwarded to `resolveWord` unchanged when supplied.
|
|
471
|
+
* @returns The resolved text, or `null` when the word is not static.
|
|
472
|
+
* @throws TypeError when `word` is not a parsed shell `Word`.
|
|
473
|
+
*/
|
|
474
|
+
resolveWordText(word, options) {
|
|
475
|
+
const resolution = resolveWord(word, options ?? this.#defaultWordOptions(word));
|
|
476
|
+
return resolution.static ? resolution.text : null;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* The `resolveWord` options implied by `word`'s grammar position when the
|
|
480
|
+
* caller supplies none: command-argument context for a `CallExpr` argument,
|
|
481
|
+
* otherwise `resolveWord`'s own conservative default.
|
|
482
|
+
*/
|
|
483
|
+
#defaultWordOptions(word) {
|
|
484
|
+
const parent = this.getParent(word);
|
|
485
|
+
const isCommandArgument = parent?.type === 'CallExpr' && Array.isArray(parent.args) && parent.args.includes(word);
|
|
486
|
+
return isCommandArgument ? { context: 'command-argument' } : undefined;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Returns the static text of `callExpr`'s command word (`argv[0]`) as
|
|
490
|
+
* written, or `null` when that word is dynamic or absent. Transparent
|
|
491
|
+
* wrappers are **not** followed: `sudo rm x` yields `"sudo"`. Use
|
|
492
|
+
* {@link ShellSourceCode.resolveInvokedCommand} for the command a wrapper
|
|
493
|
+
* chain actually runs.
|
|
494
|
+
*
|
|
495
|
+
* Delegates to `sh-ast/analyze`: the answer is the `argv0` resolution of the
|
|
496
|
+
* `CommandSite` that `enumerateCommands` reports for this `CallExpr` in this
|
|
497
|
+
* file. Leading `VAR=val` assignments are not part of argv, so
|
|
498
|
+
* `FOO=1 jq .` yields `"jq"`. An assignment-only statement (`FOO=1`) has no
|
|
499
|
+
* argv0 and no `CommandSite`, and yields `null`.
|
|
500
|
+
*
|
|
501
|
+
* @param callExpr - A parsed `CallExpr` node from this file's lint tree.
|
|
502
|
+
* @returns The command word's static text, or `null`.
|
|
503
|
+
* @throws TypeError when `callExpr` is not a parsed shell `CallExpr`.
|
|
504
|
+
* @throws `ShAnalyzeMaxDepthError` (from `sh-ast/analyze`) when this file's
|
|
505
|
+
* nesting exceeds analysis's recursion guard; see
|
|
506
|
+
* {@link ShellSourceCode.resolveInvokedCommand}.
|
|
507
|
+
*/
|
|
508
|
+
getCommandName(callExpr) {
|
|
509
|
+
const argv0 = this.#commandSiteFor(callExpr)?.argv0;
|
|
510
|
+
return argv0?.static ? argv0.text : null;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Follows `callExpr`'s argv0 through transparent wrappers (`env`, `sudo`,
|
|
514
|
+
* `nohup`, `exec`, β¦) and returns `sh-ast/analyze`'s full `resolveArgv0`
|
|
515
|
+
* result, or `null` for a `CallExpr` that has no `CommandSite` (an
|
|
516
|
+
* assignment-only statement such as `FOO=1`).
|
|
517
|
+
*
|
|
518
|
+
* Delegates to `resolveArgv0` with the `CommandSite` that `enumerateCommands`
|
|
519
|
+
* reports for this `CallExpr` in this file, so it inherits that function's
|
|
520
|
+
* guarantee that an unknowable word is never guessed through. Read the
|
|
521
|
+
* effective command's name as
|
|
522
|
+
* `res.effective.static ? res.effective.text : null`.
|
|
523
|
+
* `sudo -u x "$prog"` and `exec "$@"` have a non-static `effective` with
|
|
524
|
+
* reason `expansion`; `env -S 'β¦'` has reason `embedded-command`; `chain`
|
|
525
|
+
* lists every word resolved along the way, outermost wrapper first, and
|
|
526
|
+
* `chainIndices` gives each entry's position in the command's `argv` (as
|
|
527
|
+
* {@link ShellSourceCode.getCommandSites} reports it). When `effective` is
|
|
528
|
+
* static, the effective command's arguments are
|
|
529
|
+
* `argv.slice(chainIndices.at(-1)! + 1)`: every entry after the effective
|
|
530
|
+
* command's own index.
|
|
531
|
+
*
|
|
532
|
+
* Enumeration runs once per file, the first time any command helper is
|
|
533
|
+
* called. If the file's nesting exceeds `enumerateCommands`'s recursion
|
|
534
|
+
* guard, its `ShAnalyzeMaxDepthError` propagates unchanged and ESLint
|
|
535
|
+
* reports the rule as crashed for that file. That fails closed: a partial
|
|
536
|
+
* index would silently omit real commands.
|
|
537
|
+
*
|
|
538
|
+
* @param callExpr - A parsed `CallExpr` node from this file's lint tree.
|
|
539
|
+
* @param options - Forwarded to `resolveArgv0` unchanged.
|
|
540
|
+
* `transparentWrappers` replaces `sh-ast/analyze`'s
|
|
541
|
+
* `DEFAULT_TRANSPARENT_WRAPPERS`; spread the defaults into the array to
|
|
542
|
+
* extend them instead.
|
|
543
|
+
* @returns The resolution, or `null` when `callExpr` invokes no command.
|
|
544
|
+
* @throws TypeError when `callExpr` is not a parsed shell `CallExpr`.
|
|
545
|
+
* @throws `ShAnalyzeMaxDepthError` (from `sh-ast/analyze`) when this file's
|
|
546
|
+
* nesting exceeds analysis's recursion guard.
|
|
547
|
+
* @throws `ShAnalyzeInvalidWrapperSpecError` (from `sh-ast/analyze`) when
|
|
548
|
+
* `options.transparentWrappers` contains a malformed entry.
|
|
549
|
+
*/
|
|
550
|
+
resolveInvokedCommand(callExpr, options) {
|
|
551
|
+
const site = this.#commandSiteFor(callExpr);
|
|
552
|
+
return site ? resolveArgv0(site, options) : null;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Returns every command invocation in this file, one entry per
|
|
556
|
+
* `sh-ast/analyze` `CommandSite`, in source order. Use it when a rule needs
|
|
557
|
+
* the whole file's commands with their structural context, such as every
|
|
558
|
+
* command inside a function or inside `$(β¦)`, instead of visiting
|
|
559
|
+
* `CallExpr` nodes one by one.
|
|
560
|
+
*
|
|
561
|
+
* The data comes from `sh-ast/analyze`'s `enumerateCommands`: facts about
|
|
562
|
+
* how each command is reached and what its words resolve to, never a
|
|
563
|
+
* verdict. Each entry's `context`, `argv0`, and `argv` are analysis's own
|
|
564
|
+
* values, unchanged; `context` lists frames outermost first, as in sh-ast.
|
|
565
|
+
* Each entry's `node` is the `CallExpr` view node (`===` the node ESLint
|
|
566
|
+
* passes to a `CallExpr` visitor), so it can be passed to
|
|
567
|
+
* `context.report()`.
|
|
568
|
+
*
|
|
569
|
+
* An assignment-only statement (`FOO=1`) runs no command and has no entry,
|
|
570
|
+
* although a command substitution in its value (`FOO=$(date)`) does.
|
|
571
|
+
*
|
|
572
|
+
* Enumeration runs at most once per file, shared with
|
|
573
|
+
* {@link ShellSourceCode.getCommandName} and
|
|
574
|
+
* {@link ShellSourceCode.resolveInvokedCommand}, and every call returns the
|
|
575
|
+
* same array. Because every rule shares it, the array and everything in each
|
|
576
|
+
* entry except `node` are deeply frozen (see `ShellCommandSite`): no rule can
|
|
577
|
+
* change what another rule reads. If the file's nesting exceeds `enumerateCommands`'s
|
|
578
|
+
* recursion guard, its `ShAnalyzeMaxDepthError` propagates unchanged, as it
|
|
579
|
+
* does for those helpers: a partial list would silently omit real commands.
|
|
580
|
+
*
|
|
581
|
+
* @returns This file's command sites, in source order.
|
|
582
|
+
* @throws `ShAnalyzeMaxDepthError` (from `sh-ast/analyze`) when this file's
|
|
583
|
+
* nesting exceeds analysis's recursion guard.
|
|
584
|
+
*/
|
|
585
|
+
getCommandSites() {
|
|
586
|
+
this.#commandSiteViews ??= viewCommandSites(this.ast, this.#getCommandSiteIndex());
|
|
587
|
+
return this.#commandSiteViews;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Returns the sh-ast record `node` was projected from: this file's original
|
|
591
|
+
* parse of `node`, with every field the lint view leaves out, including
|
|
592
|
+
* named token positions such as `BinaryCmd.OpPos`, `Stmt.Semicolon` and
|
|
593
|
+
* `Redirect.OpPos`. Pass those positions to
|
|
594
|
+
* {@link ShellSourceCode.getIndexFromSyntaxPos} or
|
|
595
|
+
* {@link ShellSourceCode.getLocFromSyntaxPos} to report on them, or pass the
|
|
596
|
+
* record to an `sh-ast/analyze` function.
|
|
597
|
+
*
|
|
598
|
+
* The returned record is read-only by contract. It is the language's
|
|
599
|
+
* original parse, shared by every rule and helper linting this file, and
|
|
600
|
+
* mutating it is unsupported: helpers such as
|
|
601
|
+
* {@link ShellSourceCode.resolveWordText} read the same record and would see
|
|
602
|
+
* the edit. It is not frozen, so a write does not throw. Make edits on an
|
|
603
|
+
* editable copy from {@link ShellSourceCode.getEditableSyntax} instead.
|
|
604
|
+
*
|
|
605
|
+
* @param node - A view node from this file's lint tree.
|
|
606
|
+
* @returns The original sh-ast record, or `undefined` when `node` has none
|
|
607
|
+
* in this file: an auxiliary view (a record whose type is not an sh-ast
|
|
608
|
+
* `Node`, such as the `Slice` of `${x:1:2}`), a view node from another
|
|
609
|
+
* file's tree, or an
|
|
610
|
+
* object that did not come from the parser.
|
|
611
|
+
*/
|
|
612
|
+
getSyntax(node) {
|
|
613
|
+
const syntax = syntaxForNode(node);
|
|
614
|
+
// A view node of another file also has a record; it is not this file's.
|
|
615
|
+
return syntax !== undefined && viewForSyntax(this.ast, syntax) === node ? syntax : undefined;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Returns the lint view node projected from `syntax`: the inverse of
|
|
619
|
+
* {@link ShellSourceCode.getSyntax}. The result is the same object (`===`)
|
|
620
|
+
* ESLint passes to rule visitors, so it can be passed to
|
|
621
|
+
* `context.report()`. Use it to come back to the view after navigating the
|
|
622
|
+
* original records, for example from a `Stmt` record's `Cmd`.
|
|
623
|
+
*
|
|
624
|
+
* Lookup is by object identity, so only the parser's own records match.
|
|
625
|
+
* sh-ast's `walk` passes its callback a fresh copy of each comment stored
|
|
626
|
+
* on a `Stmt`, `CaseItem` or `ArrayElem`, as Go's `syntax.Walk` does, so
|
|
627
|
+
* those `Comment` copies have no view node. Read the stored comment from
|
|
628
|
+
* the record's `Comments` array instead.
|
|
629
|
+
*
|
|
630
|
+
* @param syntax - An sh-ast `Node` record from this file's original parse.
|
|
631
|
+
* @returns The view node, or `undefined` for any other record, including a
|
|
632
|
+
* copy of one of this file's records (such as a `structuredClone`) and a
|
|
633
|
+
* record from another parse.
|
|
634
|
+
*/
|
|
635
|
+
getViewNode(syntax) {
|
|
636
|
+
return viewForSyntax(this.ast, syntax);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Returns an independent, editable deep copy of the sh-ast record `node`
|
|
640
|
+
* was parsed from: the first half of a printed fix. Edit the copy, then pass
|
|
641
|
+
* it to {@link ShellSourceCode.getPrintedReplacement}.
|
|
642
|
+
*
|
|
643
|
+
* The copy is `structuredClone` of the record
|
|
644
|
+
* {@link ShellSourceCode.getSyntax} returns. sh-ast documents and tests
|
|
645
|
+
* ordinary `structuredClone` as the supported way to make an independent
|
|
646
|
+
* editable copy of a syntax subtree (sh-ast 0.7.0 README, "Editing and
|
|
647
|
+
* printing"), so no second parse is involved. The copy owns its records,
|
|
648
|
+
* arrays, positions and byte buffers. Editing it leaves this file's original
|
|
649
|
+
* syntax, its lint view, its cached traversal and the answers of view-aware
|
|
650
|
+
* helpers such as `resolveWord` and
|
|
651
|
+
* {@link ShellSourceCode.resolveWordText} unchanged. Each call returns a new
|
|
652
|
+
* copy.
|
|
653
|
+
*
|
|
654
|
+
* The copy's position fields keep their original values, and the copy has
|
|
655
|
+
* no view node: {@link ShellSourceCode.getViewNode},
|
|
656
|
+
* {@link ShellSourceCode.getIndexFromSyntaxPos} and the parse's position
|
|
657
|
+
* snapshot describe the original source, not the edit. Keep `node` to know
|
|
658
|
+
* which source range the fix replaces.
|
|
659
|
+
*
|
|
660
|
+
* @param node - A view node from this file's lint tree.
|
|
661
|
+
* @returns A deep copy of `node`'s original sh-ast record.
|
|
662
|
+
* @throws TypeError when `node` has no original record in this file: an
|
|
663
|
+
* auxiliary view (such as the `Slice` of `${x:1:2}`), a view node of another
|
|
664
|
+
* file, or an object that did not come from the parser.
|
|
665
|
+
*/
|
|
666
|
+
getEditableSyntax(node) {
|
|
667
|
+
return structuredClone(this.#originalSyntaxFor(node));
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Prints `edited` with sh-ast's `printSync` and pairs the text with the
|
|
671
|
+
* source range it replaces, so a printed fix is
|
|
672
|
+
* `fix: (fixer) => fixer.replaceTextRange(r.range, r.text)`. `edited` is
|
|
673
|
+
* usually a copy from {@link ShellSourceCode.getEditableSyntax} of `node`
|
|
674
|
+
* or of one of its ancestors.
|
|
675
|
+
*
|
|
676
|
+
* The range is `node.range`, except that a `File` root replaces the whole
|
|
677
|
+
* text (`[0, text.length]`) whatever `node` is, which allows whole-file
|
|
678
|
+
* regeneration. `options` is passed to `printSync` unchanged.
|
|
679
|
+
*
|
|
680
|
+
* The printer formats; it does not replay the source. Its output starts at
|
|
681
|
+
* column 0 and indents nested lines with upstream's indentation (tabs by
|
|
682
|
+
* default, `Indent` spaces when `options.Indent` is set). Replacing a
|
|
683
|
+
* multi-line root nested inside other code, such as an `if` statement in a
|
|
684
|
+
* function body, therefore keeps the shell meaning but may change the
|
|
685
|
+
* formatting of its lines. Use a `Word` or `WordPart` root for a surgical
|
|
686
|
+
* fix that leaves the surrounding source as written.
|
|
687
|
+
*
|
|
688
|
+
* A `Stmt` root is adjusted so its text matches `node.range`. A statement's
|
|
689
|
+
* own leading and trailing comments lie outside its range, so they are not
|
|
690
|
+
* printed (the copy passed in is not modified). When `node` is a statement
|
|
691
|
+
* whose range ends in its `;` or `&` terminator, upstream prints the
|
|
692
|
+
* statement without a `;`, so a `;` is appended unless the edited statement
|
|
693
|
+
* prints its own `&`, `|&` or disown operator. Without this, replacing `a`
|
|
694
|
+
* in `a; b` would produce `a b`.
|
|
695
|
+
*
|
|
696
|
+
* @param node - A view node from this file's lint tree: the node the fix
|
|
697
|
+
* replaces.
|
|
698
|
+
* @param edited - The record to print: a `File`, `Stmt`, `Word`, `Assign`,
|
|
699
|
+
* `Command` or `WordPart`.
|
|
700
|
+
* @param options - Forwarded to `printSync` unchanged.
|
|
701
|
+
* @returns The range to replace and the printed text to put there.
|
|
702
|
+
* @throws TypeError when `node` has no original record in this file (see
|
|
703
|
+
* {@link ShellSourceCode.getEditableSyntax}), when `edited` is not a
|
|
704
|
+
* printable root (the message names its type), or when the printer's output
|
|
705
|
+
* is not valid UTF-8. Non-UTF-8 output comes back from `printSync` as a
|
|
706
|
+
* `Uint8Array` and is never decoded lossily into a fix.
|
|
707
|
+
* @throws `PrintError` or `PrintPanicError` (from sh-ast) when upstream's
|
|
708
|
+
* printer fails for `edited` or `options`.
|
|
709
|
+
*/
|
|
710
|
+
getPrintedReplacement(node, edited, options) {
|
|
711
|
+
const original = this.#originalSyntaxFor(node);
|
|
712
|
+
const rootType = printableRootType(edited);
|
|
713
|
+
const root = edited.Type === 'Stmt' ? { ...edited, Comments: null } : edited;
|
|
714
|
+
const printed = printSync(root, options);
|
|
715
|
+
if (typeof printed !== 'string') {
|
|
716
|
+
throw new TypeError('printSync produced output that is not valid UTF-8; a text fix cannot hold it without ' +
|
|
717
|
+
'lossy decoding');
|
|
718
|
+
}
|
|
719
|
+
if (rootType === 'File') {
|
|
720
|
+
return { range: [0, this.text.length], text: printed };
|
|
721
|
+
}
|
|
722
|
+
const dropsTerminator = edited.Type === 'Stmt' &&
|
|
723
|
+
original.Type === 'Stmt' &&
|
|
724
|
+
original.Semicolon.IsValid &&
|
|
725
|
+
!printsOwnTerminator(edited);
|
|
726
|
+
return {
|
|
727
|
+
range: [node.range[0], node.range[1]],
|
|
728
|
+
text: dropsTerminator ? `${printed};` : printed,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* `node`'s original record in this file, or a `TypeError` when it has none,
|
|
733
|
+
* so the printed-fix helpers never copy or replace a range they cannot tie
|
|
734
|
+
* to this file's parse.
|
|
735
|
+
*/
|
|
736
|
+
#originalSyntaxFor(node) {
|
|
737
|
+
const syntax = this.getSyntax(node);
|
|
738
|
+
if (syntax === undefined) {
|
|
739
|
+
throw new TypeError('expected a view node parsed from this file; auxiliary views, view nodes of other ' +
|
|
740
|
+
'files and objects that did not come from the parser have no original syntax');
|
|
741
|
+
}
|
|
742
|
+
return syntax;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Converts a byte position from this file's original parse into the
|
|
746
|
+
* UTF-16 index ESLint uses for `range` and `text.slice`. sh-ast positions
|
|
747
|
+
* count UTF-8 bytes, so a raw `Offset` is wrong for any source with
|
|
748
|
+
* non-ASCII text before it. For example, in `echo π && echo δΈζ`,
|
|
749
|
+
* `getIndexFromSyntaxPos(binaryCmd.OpPos)` is the index where `&&` starts.
|
|
750
|
+
*
|
|
751
|
+
* The conversion is only correct for positions of this file's original
|
|
752
|
+
* parse, as read from {@link ShellSourceCode.getSyntax}'s records. The live
|
|
753
|
+
* positions of an edited or copied tree describe that edit, not this
|
|
754
|
+
* source.
|
|
755
|
+
*
|
|
756
|
+
* @param pos - A `SyntaxPos` from one of this file's original records.
|
|
757
|
+
* @returns The UTF-16 index of `pos` in this file's text.
|
|
758
|
+
* @throws RangeError when `pos` is invalid (`IsValid === false`), recovered
|
|
759
|
+
* (`IsRecovered === true`), or overflowed (`OffsetOverflow === true`), or
|
|
760
|
+
* when its offset is not a UTF-8 character boundary of this source. Such a
|
|
761
|
+
* position is never rounded to a nearby boundary.
|
|
762
|
+
* @throws TypeError when `pos` is not a `SyntaxPos`, or when this source
|
|
763
|
+
* code's tree was not built by this language's parser.
|
|
764
|
+
*/
|
|
765
|
+
getIndexFromSyntaxPos(pos) {
|
|
766
|
+
return this.#coordinateAt(pos).index;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Converts a byte position from this file's original parse into a one-based
|
|
770
|
+
* ESLint location, the shape `context.report({ loc })` takes. The column
|
|
771
|
+
* counts UTF-16 code units, not bytes, and CRLF counts as one line break.
|
|
772
|
+
* Same contract as {@link ShellSourceCode.getIndexFromSyntaxPos}.
|
|
773
|
+
*
|
|
774
|
+
* @param pos - A `SyntaxPos` from one of this file's original records.
|
|
775
|
+
* @returns The one-based line and UTF-16 column of `pos`.
|
|
776
|
+
* @throws RangeError when `pos` is invalid, recovered, or overflowed, or
|
|
777
|
+
* when its offset is not a UTF-8 character boundary of this source.
|
|
778
|
+
* @throws TypeError when `pos` is not a `SyntaxPos`, or when this source
|
|
779
|
+
* code's tree was not built by this language's parser.
|
|
780
|
+
*/
|
|
781
|
+
getLocFromSyntaxPos(pos) {
|
|
782
|
+
const { line, column } = this.#coordinateAt(pos);
|
|
783
|
+
return { line, column };
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Converts `pos` with the coordinate index projection built for this
|
|
787
|
+
* file's source, so the file pays for one index however many positions
|
|
788
|
+
* rules convert.
|
|
789
|
+
*/
|
|
790
|
+
#coordinateAt(pos) {
|
|
791
|
+
const coordinates = projectedCoordinates(this.ast);
|
|
792
|
+
if (coordinates === undefined) {
|
|
793
|
+
throw new TypeError('expected a parsed shell File from this language adapter');
|
|
794
|
+
}
|
|
795
|
+
return coordinates.fromByteOffset(exactByteOffset(pos));
|
|
796
|
+
}
|
|
797
|
+
/** This file's command-site index, built by one `enumerateCommands` call on first use. */
|
|
798
|
+
#getCommandSiteIndex() {
|
|
799
|
+
this.#commandSites ??= buildCommandSiteIndex(this.ast);
|
|
800
|
+
return this.#commandSites;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Looks up `callExpr`'s `CommandSite` in this file's lazily built index.
|
|
804
|
+
* The `CallExpr` is validated before the index is built, so a misuse
|
|
805
|
+
* reports `TypeError` rather than an unrelated analysis error.
|
|
806
|
+
*/
|
|
807
|
+
#commandSiteFor(callExpr) {
|
|
808
|
+
const raw = rawCallExprFor(callExpr);
|
|
809
|
+
return this.#getCommandSiteIndex().byNode.get(raw);
|
|
810
|
+
}
|
|
277
811
|
/**
|
|
278
812
|
* Returns the parent of `node`, or `undefined` for the root `File` node
|
|
279
813
|
* (or any node visited before `traverse()` has run once). ESLint always
|
|
@@ -300,19 +834,8 @@ export class ShellSourceCode extends TextSourceCodeBase {
|
|
|
300
834
|
this.#parents.set(node, parent);
|
|
301
835
|
}
|
|
302
836
|
steps.push(new VisitNodeStep({ target: node, phase: 1, args: [node, parent] }));
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
const value = node[key];
|
|
306
|
-
if (Array.isArray(value)) {
|
|
307
|
-
for (const child of value) {
|
|
308
|
-
if (isShellNode(child)) {
|
|
309
|
-
visit(child, node);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
else if (isShellNode(value)) {
|
|
314
|
-
visit(value, node);
|
|
315
|
-
}
|
|
837
|
+
for (const child of childNodes(node)) {
|
|
838
|
+
visit(child, node);
|
|
316
839
|
}
|
|
317
840
|
steps.push(new VisitNodeStep({ target: node, phase: 2, args: [node, parent] }));
|
|
318
841
|
};
|
|
@@ -323,15 +846,22 @@ export class ShellSourceCode extends TextSourceCodeBase {
|
|
|
323
846
|
* Returns retained comments in source order. Reflection-field order can place
|
|
324
847
|
* a later enclosing comment before an earlier nested comment, so sort by the
|
|
325
848
|
* exact derived source span before directive and inline-config processing.
|
|
849
|
+
*
|
|
850
|
+
* A tree built by the parser already recorded its comments during projection,
|
|
851
|
+
* so no extra tree walk is needed. Only a tree constructed some other way
|
|
852
|
+
* (for example, passed directly to the constructor) is walked to find them.
|
|
326
853
|
*/
|
|
327
854
|
#getComments() {
|
|
328
855
|
if (!this.#comments) {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
856
|
+
const projected = projectedComments(this.ast);
|
|
857
|
+
const comments = projected === undefined ? [] : [...projected];
|
|
858
|
+
if (projected === undefined) {
|
|
859
|
+
walkShell(this.ast, (node) => {
|
|
860
|
+
if (isShellComment(node)) {
|
|
861
|
+
comments.push(node);
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
}
|
|
335
865
|
comments.sort((a, b) => a.range[0] - b.range[0] || a.range[1] - b.range[1]);
|
|
336
866
|
this.#comments = comments;
|
|
337
867
|
}
|
|
@@ -418,4 +948,3 @@ export class ShellSourceCode extends TextSourceCodeBase {
|
|
|
418
948
|
return { configs, problems };
|
|
419
949
|
}
|
|
420
950
|
}
|
|
421
|
-
//# sourceMappingURL=shell-source-code.js.map
|