@gotgenes/pi-permission-system 33.1.1 → 34.0.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/CHANGELOG.md +11 -0
- package/docs/configuration.md +4 -1
- package/package.json +1 -1
- package/src/access-intent/bash/bash-path-resolver.ts +34 -15
- package/src/access-intent/bash/node-text.ts +36 -0
- package/src/access-intent/bash/redirect-analysis.ts +31 -2
- package/src/access-intent/bash/token-collection.ts +73 -10
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [34.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v33.1.1...pi-permission-system-v34.0.0) (2026-09-25)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** **breaking:** check a redirect's target against path rules even when the file does not exist yet ([aa88e23](https://github.com/gotgenes/pi-packages/commit/aa88e23be9d6d994728855000eae95e00e0ca03e)), closes [#609](https://github.com/gotgenes/pi-packages/issues/609)
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
* **pi-permission-system:** record that a redirect's target is projected by its role ([c709401](https://github.com/gotgenes/pi-packages/commit/c70940154c63bb8d1c0b92ca1ef262ec22215102)), closes [#609](https://github.com/gotgenes/pi-packages/issues/609)
|
|
18
|
+
|
|
8
19
|
## [33.1.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v33.1.0...pi-permission-system-v33.1.1) (2026-09-24)
|
|
9
20
|
|
|
10
21
|
|
package/docs/configuration.md
CHANGED
|
@@ -739,7 +739,7 @@ Quoting is understood, so `ls "$HOME/x"` and `ls $HOME/x` are treated alike.
|
|
|
739
739
|
|
|
740
740
|
What the bash projection resolves:
|
|
741
741
|
|
|
742
|
-
- Absolute, home-relative (`~/`), parent-traversal (`../`), and separator-bearing tokens, plus redirect targets (`> out.txt
|
|
742
|
+
- Absolute, home-relative (`~/`), parent-traversal (`../`), and separator-bearing tokens, plus redirect targets (`> out.txt`, including a file the redirect creates) and values embedded in long options (`--file=/tmp/patterns`).
|
|
743
743
|
- The plain shell variables `$HOME` / `${HOME}` and `$PWD` / `${PWD}`, so `$HOME/x` is gated exactly as `~/x` and the literal absolute spelling, whether or not the target exists.
|
|
744
744
|
- Relative tokens, against the working directory produced by folding literal current-shell `cd` commands.
|
|
745
745
|
- A bare token (`cat id_rsa`) when it names an existing filesystem entry.
|
|
@@ -860,6 +860,9 @@ A tool's identity establishes its direction, and on the bash surface a redirect
|
|
|
860
860
|
An access whose direction cannot be established consults **both** surfaces and takes the more restrictive answer.
|
|
861
861
|
That is deliberate: an unproven access is never treated as the narrower one.
|
|
862
862
|
|
|
863
|
+
A redirect's target reaches its surface whether or not the file exists yet, so a `path_write` pattern governs what a command may create through `>`, not only what it may overwrite.
|
|
864
|
+
Only the redirect's literal target counts: a computed one (`> "$OUT"`) is not resolved, as for any other computed path.
|
|
865
|
+
|
|
863
866
|
A redirect the parser could not make sense of is unproven for the same reason.
|
|
864
867
|
The read-write open `<>` is the clearest case: `tree-sitter-bash` has no node for it, so neither half of the operator can be trusted to describe the whole, and its destination consults both surfaces rather than the one the surviving half would name.
|
|
865
868
|
The rule is about the parse rather than about `<>`, so it also covers a redirect that is itself well-formed but sits beside something the parser could not read: in `cat $(( > out.txt`, the `> out.txt` consults both surfaces too.
|
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
collectRedirectTokens,
|
|
21
21
|
extractCommandName,
|
|
22
22
|
type PathToken,
|
|
23
|
+
type TokenRole,
|
|
23
24
|
} from "./token-collection";
|
|
24
25
|
|
|
25
26
|
// ── Internal types ───────────────────────────────────────────────────────────
|
|
@@ -40,13 +41,14 @@ type EffectiveBase =
|
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* A path-candidate token paired with the effective working directory projected
|
|
43
|
-
* onto the point in the command stream where it appears,
|
|
44
|
-
* position proved.
|
|
44
|
+
* onto the point in the command stream where it appears, the effect its
|
|
45
|
+
* position proved, and the role its collector gave it.
|
|
45
46
|
*/
|
|
46
47
|
interface PathCandidate {
|
|
47
48
|
readonly token: string;
|
|
48
49
|
readonly base: EffectiveBase;
|
|
49
50
|
readonly effect: TokenEffect;
|
|
51
|
+
readonly role: TokenRole;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/** A promoted bare token and its resolved path, before an effect is attached. */
|
|
@@ -101,9 +103,12 @@ const UNKNOWN_BASE: EffectiveBase = { kind: "unknown" };
|
|
|
101
103
|
* decision — so no walk step re-reads the platform or threads the cwd.
|
|
102
104
|
*
|
|
103
105
|
* A bare token that fails both shape gates is admitted when the normalizer's
|
|
104
|
-
* existence probe says it names a real filesystem entry (ADR 0009, #645).
|
|
105
|
-
*
|
|
106
|
-
*
|
|
106
|
+
* existence probe says it names a real filesystem entry (ADR 0009, #645). A
|
|
107
|
+
* redirect's own target needs neither: its collector proved it names a file,
|
|
108
|
+
* so its `redirect-destination` role admits it whether or not the file exists
|
|
109
|
+
* yet (#609). The resolver consults no ruleset: candidacy is a syntax and
|
|
110
|
+
* filesystem question, and the policy decision belongs to the gates
|
|
111
|
+
* downstream.
|
|
107
112
|
*
|
|
108
113
|
* Tell-don't-ask: callers hand it a parsed tree and receive the resolved
|
|
109
114
|
* {@link ResolvedBashPaths} slices in one {@link resolve} call; the AST walk,
|
|
@@ -448,7 +453,8 @@ export class BashPathResolver {
|
|
|
448
453
|
* Project the collected candidates into deduplicated external paths.
|
|
449
454
|
*
|
|
450
455
|
* Filters candidates through the strict path classifier
|
|
451
|
-
* (`classifyTokenAsPathCandidate`)
|
|
456
|
+
* (`classifyTokenAsPathCandidate`) unless their role already admits them,
|
|
457
|
+
* resolves each against its effective working
|
|
452
458
|
* directory base, and returns only paths that resolve outside the baked cwd in
|
|
453
459
|
* their lexical (as-typed, normalized but not symlink-resolved) form.
|
|
454
460
|
*
|
|
@@ -462,8 +468,10 @@ export class BashPathResolver {
|
|
|
462
468
|
const seen = new Map<string, number>();
|
|
463
469
|
const externalPaths: BashExternalPath[] = [];
|
|
464
470
|
|
|
465
|
-
for (const { token, base, effect } of candidates) {
|
|
466
|
-
const candidate =
|
|
471
|
+
for (const { token, base, effect, role } of candidates) {
|
|
472
|
+
const candidate = admittedByRole(role)
|
|
473
|
+
? token
|
|
474
|
+
: classifyTokenAsPathCandidate(token);
|
|
467
475
|
if (!candidate) {
|
|
468
476
|
// A bare token the strict shape gate rejects can still escape the tree
|
|
469
477
|
// through a symlink, so probe it and apply the ordinary boundary
|
|
@@ -542,7 +550,8 @@ export class BashPathResolver {
|
|
|
542
550
|
* Filters candidates through the broad path classifier
|
|
543
551
|
* (`classifyTokenAsRuleCandidate`), falling back to {@link probeBareToken}
|
|
544
552
|
* for a bare token the broad classifier rejects for shape — admitted only
|
|
545
|
-
* when it names an existing filesystem entry (#645).
|
|
553
|
+
* when it names an existing filesystem entry (#645). A token its role
|
|
554
|
+
* admits skips both (#609).
|
|
546
555
|
* On win32 the broad classifier is told to treat a backslash as a path
|
|
547
556
|
* separator, so a backslash-relative token (`dir\file`) is recognized as a
|
|
548
557
|
* rule candidate the same as its forward-slash equivalent (#520); on POSIX
|
|
@@ -558,11 +567,10 @@ export class BashPathResolver {
|
|
|
558
567
|
const seen = new Map<string, number>();
|
|
559
568
|
const result: BashPathRuleCandidate[] = [];
|
|
560
569
|
|
|
561
|
-
for (const { token, base, effect } of candidates) {
|
|
562
|
-
const shaped =
|
|
563
|
-
token
|
|
564
|
-
this.normalizer.flavor
|
|
565
|
-
);
|
|
570
|
+
for (const { token, base, effect, role } of candidates) {
|
|
571
|
+
const shaped = admittedByRole(role)
|
|
572
|
+
? token
|
|
573
|
+
: classifyTokenAsRuleCandidate(token, this.normalizer.flavor);
|
|
566
574
|
const probed =
|
|
567
575
|
shaped === null
|
|
568
576
|
? this.probeBareToken(token, base)
|
|
@@ -677,7 +685,18 @@ function tagTokens(
|
|
|
677
685
|
base: EffectiveBase,
|
|
678
686
|
out: PathCandidate[],
|
|
679
687
|
): void {
|
|
680
|
-
for (const { token, effect } of tokens)
|
|
688
|
+
for (const { token, effect, role } of tokens) {
|
|
689
|
+
out.push({ token, base, effect, role });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Whether a candidate's role settles its candidacy, so neither shape gate nor
|
|
695
|
+
* the existence probe is consulted: a redirect's own target is proven by
|
|
696
|
+
* syntax to name a file, including one the command is about to create.
|
|
697
|
+
*/
|
|
698
|
+
function admittedByRole(role: TokenRole): boolean {
|
|
699
|
+
return role === "redirect-destination";
|
|
681
700
|
}
|
|
682
701
|
|
|
683
702
|
/**
|
|
@@ -27,6 +27,42 @@ export const ARG_NODE_TYPES = new Set([
|
|
|
27
27
|
"raw_string",
|
|
28
28
|
]);
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Whether an argument node's value is decided at run time: it contains a
|
|
32
|
+
* command or process substitution, an arithmetic expansion, or a variable
|
|
33
|
+
* expansion {@link resolvePlainVariableExpansion} cannot resolve.
|
|
34
|
+
*
|
|
35
|
+
* The complement of what {@link resolveNodeText} can spell exactly. A plain
|
|
36
|
+
* `$HOME` / `$PWD` reference resolves, so `"$HOME/out"` is not computed; any
|
|
37
|
+
* other expansion falls back to its own source text there, which names a file
|
|
38
|
+
* that is not the one the shell will touch (ADR 0009's computed-path residual).
|
|
39
|
+
* A single-quoted `'$x'` is a literal.
|
|
40
|
+
*/
|
|
41
|
+
export function hasComputedPart(node: TSNode): boolean {
|
|
42
|
+
if (COMPUTED_NODE_TYPES.has(node.type)) return true;
|
|
43
|
+
if (VARIABLE_EXPANSION_TYPES.has(node.type)) {
|
|
44
|
+
return resolvePlainVariableExpansion(node) === null;
|
|
45
|
+
}
|
|
46
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
47
|
+
const child = node.child(i);
|
|
48
|
+
if (child && hasComputedPart(child)) return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Node types whose value only running the command can produce. */
|
|
54
|
+
const COMPUTED_NODE_TYPES: ReadonlySet<string> = new Set([
|
|
55
|
+
"command_substitution",
|
|
56
|
+
"process_substitution",
|
|
57
|
+
"arithmetic_expansion",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/** Variable references, computed unless they resolve as a plain reference. */
|
|
61
|
+
const VARIABLE_EXPANSION_TYPES: ReadonlySet<string> = new Set([
|
|
62
|
+
"simple_expansion",
|
|
63
|
+
"expansion",
|
|
64
|
+
]);
|
|
65
|
+
|
|
30
66
|
/**
|
|
31
67
|
* Resolve the "shell value" of an argument node — the string the shell
|
|
32
68
|
* would pass to the command after quote removal.
|
|
@@ -7,8 +7,9 @@ import { parseUnresolvedAt, type TSNode } from "./parser";
|
|
|
7
7
|
*
|
|
8
8
|
* `command-effects.ts` owns the operator *table* — which spelling means read,
|
|
9
9
|
* which means write — and this module owns reading a `file_redirect` node well
|
|
10
|
-
* enough to consult it: finding the operator among the node's children,
|
|
11
|
-
* telling a destination that names a file from one that names a descriptor
|
|
10
|
+
* enough to consult it: finding the operator among the node's children,
|
|
11
|
+
* telling a destination that names a file from one that names a descriptor,
|
|
12
|
+
* and naming which destination is the redirect's own target.
|
|
12
13
|
*
|
|
13
14
|
* The split exists because two callers need different answers from the same
|
|
14
15
|
* read, and — importantly — they need them under different burdens of proof.
|
|
@@ -92,6 +93,34 @@ export function redirectMayWriteFile(redirect: TSNode): boolean {
|
|
|
92
93
|
return false;
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The child index of the node `redirect` reads or writes (its first named
|
|
98
|
+
* child after the operator), or `undefined` when it names none (`>&-`).
|
|
99
|
+
*
|
|
100
|
+
* The operator is the redirect's only unnamed child, and a source descriptor
|
|
101
|
+
* (`2` in `2>`) precedes it, so the first named child after it is the
|
|
102
|
+
* target without asking its type.
|
|
103
|
+
*
|
|
104
|
+
* Only the first: tree-sitter-bash 0.25.1 declares the destination
|
|
105
|
+
* `repeat1`, so the words after it in `grep pat 2>/dev/null f.txt` parse as
|
|
106
|
+
* further destinations, while bash passes them to the redirected command as
|
|
107
|
+
* arguments (#977). An index rather than a node, because a caller iterating
|
|
108
|
+
* the children compares positions rather than wrapper identity.
|
|
109
|
+
*/
|
|
110
|
+
export function redirectTargetIndex(redirect: TSNode): number | undefined {
|
|
111
|
+
let seenOperator = false;
|
|
112
|
+
for (let i = 0; i < redirect.childCount; i++) {
|
|
113
|
+
const child = redirect.child(i);
|
|
114
|
+
if (!child) continue;
|
|
115
|
+
if (!child.isNamed) {
|
|
116
|
+
seenOperator = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (seenOperator) return i;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
95
124
|
/**
|
|
96
125
|
* Destination node types that name a file descriptor rather than a file, so
|
|
97
126
|
* `>&` / `<&` duplicate a stream instead of touching the filesystem.
|
|
@@ -4,14 +4,31 @@ import { proveCommandEffect } from "./command-effects";
|
|
|
4
4
|
import { EXECUTION_HOST_TYPES, forEachExecutionIn } from "./nested-execution";
|
|
5
5
|
import {
|
|
6
6
|
ARG_NODE_TYPES,
|
|
7
|
+
hasComputedPart,
|
|
7
8
|
resolveNodeText,
|
|
8
9
|
SKIP_SUBTREE_TYPES,
|
|
9
10
|
} from "./node-text";
|
|
10
11
|
import type { TSNode } from "./parser";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
redirectEffectForDestination,
|
|
14
|
+
redirectTargetIndex,
|
|
15
|
+
} from "./redirect-analysis";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What a collected token is, as the collector that produced it established.
|
|
19
|
+
*
|
|
20
|
+
* The role decides candidacy and the effect decides direction; both are
|
|
21
|
+
* stamped at the same site and never re-derived downstream.
|
|
22
|
+
*/
|
|
23
|
+
export type TokenRole =
|
|
24
|
+
/** An operand of unknown path-hood: shape and the existence probe decide. */
|
|
25
|
+
| "operand"
|
|
26
|
+
/** A redirect's own target, which the syntax proves names a file. */
|
|
27
|
+
| "redirect-destination";
|
|
12
28
|
|
|
13
29
|
/**
|
|
14
|
-
* A collected path-candidate token paired with the effect its position proved
|
|
30
|
+
* A collected path-candidate token paired with the effect its position proved
|
|
31
|
+
* and the role its collector gave it.
|
|
15
32
|
*
|
|
16
33
|
* The pairing is made where the token is *produced*, never by mapping a whole
|
|
17
34
|
* result: a nested execution's tokens carry their own command's attribution
|
|
@@ -20,6 +37,7 @@ import { redirectEffectForDestination } from "./redirect-analysis";
|
|
|
20
37
|
export interface PathToken {
|
|
21
38
|
readonly token: string;
|
|
22
39
|
readonly effect: TokenEffect;
|
|
40
|
+
readonly role: TokenRole;
|
|
23
41
|
}
|
|
24
42
|
|
|
25
43
|
// ── Public surface ─────────────────────────────────────────────────────────
|
|
@@ -108,23 +126,55 @@ export function collectCommandTokens(node: TSNode): PathToken[] {
|
|
|
108
126
|
* in front of it is. A destination the operator names as a file descriptor
|
|
109
127
|
* (`2>&1`) contributes no token at all.
|
|
110
128
|
*
|
|
129
|
+
* The redirect's own target carries the `redirect-destination` role when the
|
|
130
|
+
* syntax proves it names a file, so the projection admits it whether or not
|
|
131
|
+
* the file exists yet (#609). Every other child is an `operand`: a word the
|
|
132
|
+
* grammar appends after the target belongs to the redirected command (#977).
|
|
133
|
+
*
|
|
111
134
|
* Reading the redirect node itself belongs to `redirect-analysis.ts`, which
|
|
112
135
|
* the command enumerator consults for the same fact (#803).
|
|
113
136
|
*/
|
|
114
137
|
export function collectRedirectTokens(node: TSNode): PathToken[] {
|
|
138
|
+
const target = redirectTargetIndex(node);
|
|
115
139
|
const tokens: PathToken[] = [];
|
|
116
140
|
for (let i = 0; i < node.childCount; i++) {
|
|
117
141
|
const child = node.child(i);
|
|
118
142
|
if (!child) continue;
|
|
119
143
|
if (ARG_NODE_TYPES.has(child.type)) {
|
|
120
144
|
const effect = redirectEffectForDestination(node, child);
|
|
121
|
-
if (effect)
|
|
145
|
+
if (effect) {
|
|
146
|
+
const token = resolveNodeText(child);
|
|
147
|
+
const role: TokenRole =
|
|
148
|
+
i === target && provesTarget(effect, child, token)
|
|
149
|
+
? "redirect-destination"
|
|
150
|
+
: "operand";
|
|
151
|
+
tokens.push({ token, effect, role });
|
|
152
|
+
}
|
|
122
153
|
}
|
|
123
154
|
tokens.push(...collectHostedExecutionTokens(child));
|
|
124
155
|
}
|
|
125
156
|
return tokens;
|
|
126
157
|
}
|
|
127
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Whether a redirect's target is proven to name this literal file.
|
|
161
|
+
*
|
|
162
|
+
* The operator must have proved an effect: a redirect the parse could not
|
|
163
|
+
* resolve proves nothing (#814). The value must be literal: a computed one
|
|
164
|
+
* names a file only running the command decides (ADR 0009's computed-path
|
|
165
|
+
* residual). And it must be non-empty, since bash refuses `> ""` rather than
|
|
166
|
+
* writing anything.
|
|
167
|
+
*/
|
|
168
|
+
function provesTarget(
|
|
169
|
+
effect: TokenEffect,
|
|
170
|
+
destination: TSNode,
|
|
171
|
+
token: string,
|
|
172
|
+
): boolean {
|
|
173
|
+
return (
|
|
174
|
+
effect.source === "syntax" && !hasComputedPart(destination) && token !== ""
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
128
178
|
/**
|
|
129
179
|
* Collect the path-candidate tokens of every command nested inside `node`'s
|
|
130
180
|
* execution contexts, reading none of the host subtree's own text.
|
|
@@ -204,7 +254,11 @@ function collectStatementOperandTokens(
|
|
|
204
254
|
tokens.push(...collectPathCandidateTokens(child));
|
|
205
255
|
continue;
|
|
206
256
|
}
|
|
207
|
-
tokens.push({
|
|
257
|
+
tokens.push({
|
|
258
|
+
token: resolveNodeText(child),
|
|
259
|
+
effect: UNPROVEN_EFFECT,
|
|
260
|
+
role: "operand",
|
|
261
|
+
});
|
|
208
262
|
tokens.push(...collectHostedExecutionTokens(child));
|
|
209
263
|
}
|
|
210
264
|
return tokens;
|
|
@@ -323,7 +377,9 @@ function collectEmbeddedOptionValues(
|
|
|
323
377
|
if (!ARG_NODE_TYPES.has(child.type)) continue;
|
|
324
378
|
|
|
325
379
|
const value = OPTION_VALUE_PATTERN.exec(resolveNodeText(child))?.[1];
|
|
326
|
-
if (value !== undefined)
|
|
380
|
+
if (value !== undefined) {
|
|
381
|
+
values.push({ token: value, effect, role: "operand" });
|
|
382
|
+
}
|
|
327
383
|
}
|
|
328
384
|
return values;
|
|
329
385
|
}
|
|
@@ -334,7 +390,7 @@ function embeddedOptionValueToken(
|
|
|
334
390
|
effect: TokenEffect,
|
|
335
391
|
): PathToken[] {
|
|
336
392
|
const value = OPTION_VALUE_PATTERN.exec(text)?.[1];
|
|
337
|
-
return value === undefined ? [] : [{ token: value, effect }];
|
|
393
|
+
return value === undefined ? [] : [{ token: value, effect, role: "operand" }];
|
|
338
394
|
}
|
|
339
395
|
|
|
340
396
|
/**
|
|
@@ -802,7 +858,11 @@ function collectPatternCommandTokens(
|
|
|
802
858
|
break;
|
|
803
859
|
case "inline-value":
|
|
804
860
|
if (directive.role === "script-file")
|
|
805
|
-
tokens.push({
|
|
861
|
+
tokens.push({
|
|
862
|
+
token: directive.value,
|
|
863
|
+
effect,
|
|
864
|
+
role: "operand",
|
|
865
|
+
});
|
|
806
866
|
if (suppliesScript(directive.role)) hasExplicitScript = true;
|
|
807
867
|
break;
|
|
808
868
|
case "regular-flag":
|
|
@@ -819,7 +879,7 @@ function collectPatternCommandTokens(
|
|
|
819
879
|
if (!hasExplicitScript && positionalsSeen < patternPositionals) {
|
|
820
880
|
positionalsSeen++; // Skip: this is an inline pattern/script.
|
|
821
881
|
} else {
|
|
822
|
-
tokens.push({ token: text, effect });
|
|
882
|
+
tokens.push({ token: text, effect, role: "operand" });
|
|
823
883
|
}
|
|
824
884
|
// A quoted token that did not act as a flag above has its embedded value
|
|
825
885
|
// split here instead.
|
|
@@ -874,7 +934,10 @@ function dischargePendingConsumption(
|
|
|
874
934
|
): ConsumptionDischarge {
|
|
875
935
|
switch (role) {
|
|
876
936
|
case "script-file":
|
|
877
|
-
return {
|
|
937
|
+
return {
|
|
938
|
+
consumed: true,
|
|
939
|
+
token: { token: text, effect, role: "operand" },
|
|
940
|
+
};
|
|
878
941
|
case "script":
|
|
879
942
|
case "value":
|
|
880
943
|
return { consumed: true };
|
|
@@ -923,7 +986,7 @@ function collectGenericCommandTokens(
|
|
|
923
986
|
|
|
924
987
|
// Argument nodes: resolve their text and collect.
|
|
925
988
|
if (ARG_NODE_TYPES.has(child.type)) {
|
|
926
|
-
tokens.push({ token: resolveNodeText(child), effect });
|
|
989
|
+
tokens.push({ token: resolveNodeText(child), effect, role: "operand" });
|
|
927
990
|
continue;
|
|
928
991
|
}
|
|
929
992
|
|