diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md
index b95cbf3..6d25979 100644
--- a/IMPLEMENTATION_PLAN.md
+++ b/IMPLEMENTATION_PLAN.md
@@ -17,7 +17,7 @@ priorities.
> below). What remains is the downstream Netclaw integration, which needs
> actions outside this repository.
-- [ ] **v0.3 prerelease consumer-API correction.** Preserve the stable v0.2
+- [x] **v0.3 prerelease consumer-API correction.** Preserve the stable v0.2
API and the `Syntax` / `Commands` / `Clauses` ingestion lanes, but replace
the alpha-only sparse coordinate and public property-bag model before
stable v0.3. Return one parser-owned analyzed argument per authored
@@ -25,7 +25,10 @@ priorities.
reference actual ancestor nodes, and remove public syntax vocabulary
that stable v0.3 never emits. Update the OpenSpec and source mocks first;
then update implementation, snapshots, corpus DTOs, consumer guide,
- README, and Netclaw together. No compatibility shim for 0.3 alphas.
+ README, and Netclaw together. No compatibility shim for 0.3 alphas. The
+ library, specifications, snapshots, corpus DTOs, generated expectations,
+ README, consumer guide, and prerelease migration notes are synchronized;
+ Netclaw migration remains the next downstream item.
- [x] **v0.3 host-selected grammar and PowerShell dialect — library slice.** The executor
selects one top-level parser; Bash never cross-parses `pwsh` payloads and
@@ -37,7 +40,7 @@ priorities.
PowerShell 7.6.4 oracle on Ubuntu and Windows, plus native Windows
PowerShell 5.1 discovery and its dialect-routed oracle on Windows.
-- [ ] **v0.3 native-Windows Netclaw integration.** Pass the exact selected
+- [x] **v0.3 native-Windows Netclaw integration.** Pass the exact selected
shell through Netclaw's executor, approval policy, and model context;
prefer a compatible `pwsh.exe`, fall back to `powershell.exe`, and
reparse and reauthorize if executable selection changes.
@@ -132,7 +135,10 @@ priorities.
- [x] **Consumer guide.** Added `docs/CONSUMER_GUIDE.md` with the
shell-neutral security-consumer algorithm, Bash and PowerShell guidance,
worked public use cases, and immutable permalinks to Netclaw's production
- integration. Linked it from the README and aligned stale PowerShell
+ integration. Added compact input-to-result-to-policy examples for command
+ occurrences, attached arguments, bounded and zero-or-more loops, cwd
+ propagation, file and descriptor redirects, substitutions, and safe-fail
+ results. Linked it from the README and aligned stale PowerShell
prerelease/status wording in the public project docs.
- [x] **Issue #52 — hyphenated PowerShell parameters/native options.**
Preserve internal hyphens, apply bash-compatible native
@@ -157,9 +163,9 @@ priorities.
### 16. Netclaw v0.2.0 integration — SPEC.PWSH §17 #9
-- [ ] Netclaw consumes the v0.2.0 package; absorbs the `Clause` rename
+- [x] Netclaw consumes the v0.2.0 package; absorbs the `Clause` rename
*(separate repository — cannot be done here)*
-- [ ] ≥1 Netclaw integration test exercises a real PowerShell corpus entry
+- [x] ≥1 Netclaw integration test exercises a real PowerShell corpus entry
through the live matcher and gets the expected gate decision
### 17. v0.3 structured shell analysis contract — issue #72
@@ -489,7 +495,7 @@ priorities.
effects, publishes synchronous/once only for one proved target, and
publishes concurrent timing for multiple targets or enabled asynchronous
switches while keeping dynamic cardinality fail closed.
-- [ ] Deliver typed PowerShell script-block execution regions before calling
+- [x] Deliver typed PowerShell script-block execution regions before calling
tasks 7.5-7.7 complete. The corrected contract adds an execution-region
syntax node with independent origin, phase, timing, and cardinality rather than a
false shared/isolated scope flag. The inert additive public API skeleton,
@@ -615,7 +621,7 @@ priorities.
independence, semantic phase order, child process/runspace boundaries,
boundaries and the fact that the in-process `Invoke-Command` parameter
set does not support `-AsJob`.
-- [ ] Complete the stable-v0.3 Bash `for ... in` and PowerShell `foreach`
+- [x] Complete the stable-v0.3 Bash `for ... in` and PowerShell `foreach`
vertical slices without gating release on a shared-analysis refactor.
- [x] Publish `0.3.0-alpha` for the Netclaw migration gate. The bare SemVer tag
published the NuGet package, symbol package, and GitHub prerelease from
@@ -695,7 +701,7 @@ priorities.
and reserved-form boundaries, including unmodeled `time`, negation,
coprocess, and current-shell brace-group syntax, which fail closed rather
than flattening nested execution into apparent ordinary verb chains.
-- [ ] Promote the Bash command-resolution mutation cases into Netclaw's strict
+- [x] Promote the Bash command-resolution mutation cases into Netclaw's strict
allow/prompt/deny matrix before the downstream approval-fatigue gate.
- [x] Add the separately tested Bash `<<<` here-string redirect slice with
bounded operand analysis and trailing-newline semantics. Default and
diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md
index f1ce924..c972fd0 100644
--- a/PROJECT_CONTEXT.md
+++ b/PROJECT_CONTEXT.md
@@ -81,13 +81,18 @@ zero-native-deps .NET parser sized to what security gates actually need.
the dialect-matched live oracle matrix (`pwsh` for PowerShell 7 and
`powershell.exe` for Windows PowerShell 5.1 on Windows CI).
-### v0.3 (contract design)
+### v0.3 (implementation and downstream acceptance)
- Add a closed, strongly typed syntax-node hierarchy while retaining existing
`Clause` leaves.
- Add a library-owned command-occurrence projection for security consumers so
every potentially executable iterator, substitution, execution-region, and
body command in the supported grammar is evaluated exactly once.
+- Return one parser-owned analyzed argument for every authored non-cwd
+ compatibility argument. Closed value and redirect record families use
+ runtime type as their discriminant; ancestry and execution-region facts
+ reference the actual parser-owned source objects. Compatibility is required
+ against stable v0.2, not experimental v0.3 alpha surfaces.
- Add typed PowerShell execution regions for direct call/dot-source blocks,
synchronous callbacks, jobs/parallel runspaces, and initialization. Public
origin/phase/timing/cardinality facts remain separate from
@@ -114,9 +119,10 @@ zero-native-deps .NET parser sized to what security gates actually need.
Existing conservative recognition may remain; unproved receivers still
expose completely delimited bodies as incomplete regions and therefore
remain fail closed.
-- Treat `openspec/changes/v0-3-structured-shell-analysis/` and its paired design
- corpus as the review authority until the accepted contract is synchronized
- into `SPEC.md` and `SPEC.POWERSHELL.md` with the production API change.
+- Treat `SPEC.md` and `SPEC.POWERSHELL.md` as the canonical synchronized
+ contract. The accepted OpenSpec change and paired design corpus retain the
+ design rationale and verification checklist until downstream acceptance and
+ archival.
### v0.2 explicit non-goals
diff --git a/README.md b/README.md
index 9ba6949..2e0dbf3 100644
--- a/README.md
+++ b/README.md
@@ -83,7 +83,8 @@ foreach (var occurrence in parsed.Commands)
var clause = occurrence.Clause;
Console.WriteLine(
$"{occurrence.ImmediateRole} {clause.Verb.Joined} " +
- $"complete={occurrence.IsComplete} cwd={occurrence.WorkingDirectory.Kind}");
+ $"complete={occurrence.IsComplete} " +
+ $"cwd={Describe(occurrence.WorkingDirectory)}");
foreach (var arg in clause.Args.Where(a => a.IsPath))
{
@@ -91,19 +92,42 @@ foreach (var occurrence in parsed.Commands)
Console.WriteLine($" {marker}: {arg.Resolved}");
}
- foreach (var effective in occurrence.EffectiveArguments)
+ foreach (var argument in occurrence.Arguments)
{
Console.WriteLine(
- $" element[{effective.ClauseElementIndex}]: {effective.Value.Kind}");
+ $" {argument.Argument.Raw}: {Describe(argument.Value)}");
}
foreach (var redirect in occurrence.Redirects)
{
- Console.WriteLine(
- $" {redirect.Operation}: {redirect.Target.Kind} " +
- $"complete={redirect.IsComplete}");
+ Console.WriteLine($" {Describe(redirect)}");
}
}
+
+static string Describe(ShellValueDomain value) => value switch
+{
+ ShellValueDomain.Exact exact => exact.Value,
+ ShellValueDomain.FiniteSet finite => string.Join(" | ", finite.Values),
+ ShellValueDomain.PathPattern pattern => pattern.Pattern,
+ ShellValueDomain.Unknown => "unknown",
+ _ => "unknown",
+};
+
+static string Describe(RedirectAnalysis redirect) => redirect switch
+{
+ FileRedirectAnalysis file =>
+ $"{file.Mode} to {Describe(file.Target)} complete={file.IsComplete}",
+ DescriptorDuplicateRedirectAnalysis duplicate =>
+ $"duplicate to fd {duplicate.TargetDescriptor}",
+ DescriptorMoveRedirectAnalysis move =>
+ $"move to fd {move.TargetDescriptor}",
+ DescriptorCloseRedirectAnalysis => "close descriptor",
+ HereDocumentRedirectAnalysis => "heredoc stdin data",
+ HereStringRedirectAnalysis hereString =>
+ $"here-string stdin data: {Describe(hereString.Data)}",
+ UnresolvedRedirectAnalysis => "unresolved redirect",
+ _ => "unknown redirect",
+};
```
## Consumer guide
@@ -131,9 +155,11 @@ public enum PwshDialect { Unknown, PowerShell7, WindowsPowerShell51 }
public sealed record ParsedCommand { /* Source, Syntax, Commands, Clauses, IsUnparseable, … */ }
public abstract record ShellSyntaxNode;
-public sealed record CommandOccurrence { /* Clause, role, ancestry, effective values, cwd, redirects, completeness */ }
-public sealed record ShellValueDomain { /* Exact, FiniteSet, Pattern, or Unknown */ }
-public sealed record RedirectAnalysis { /* source, operation, target, heredoc facts, completeness */ }
+public sealed record CommandOccurrence { /* Clause, role, ancestry, analyzed arguments, cwd, redirects, completeness */ }
+public sealed record AnalyzedArgument { /* direct Arg + ClauseElement + ShellValueDomain join */ }
+public abstract record ShellValueDomain; // nested Unknown, Exact, FiniteSet, PathPattern
+public abstract record RedirectSource; // nested Unknown, Default, Descriptor, PowerShellAllStreams
+public abstract record RedirectAnalysis; // file, descriptor, heredoc, here-string, or unresolved alternative
public sealed record Clause { /* Operator, Verb, Args, Redirects, Elements, IsSubshell, IsCommandStringWrapped */ }
public sealed record ClauseElement { /* Raw, Value, Role, source span, verb-relative position, path facts */ }
public sealed record VerbChain { /* Tokens, Joined, CanonicalVerb, IsDynamic */ }
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 8602fc6..8614914 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,16 +1,35 @@
#### Unreleased ####
+## Changed
+
+- Replace the experimental v0.3 sparse `EffectiveArguments` coordinate overlay
+ with one parser-owned `AnalyzedArgument` per authored non-cwd argument. Each
+ entry directly references its `Arg`, source `ClauseElement`, and effective
+ value, including many-to-one inline option bindings.
+- Replace the experimental value, redirect-source, and redirect-operation
+ property bags with closed record families intended for runtime type
+ matching. Ancestry frames now reference actual syntax nodes, and execution
+ regions reference their actual host argument.
+- Remove alpha-only public syntax kinds, condition/branch vocabulary, analysis
+ limits, and other shapes the stable parser never emits. Every new v0.3 result
+ is parser-owned and every read-only list introduced by v0.3 is defensively
+ backed; stable v0.2 construction and list semantics remain unchanged.
+
## Consumer migration
- v0.3 security consumers authorize `ParsedCommand.Commands` and use
`ParsedCommand.Syntax` only for display and diagnostics. The conservative
v0.2 `Clauses` projection remains supported throughout v0.3, including every
v0.3.x release; no removal version is scheduled.
-- The additive v0.3 records and `ParsedCommand` members change generated record
+- The new v0.3 records and `ParsedCommand` members change generated record
equality, hashing, `ToString()`, and reflection-based serialization output.
ShellSyntaxTree does not define a stable serialized wire format. Persisted
results require a consumer-owned, versioned DTO or explicit serializer
- mapping that fails closed on unknown node and enum values.
+ mapping that fails closed on unknown runtime alternatives and enum values.
+- No source or binary compatibility is provided for `0.3.0-alpha.*` packages.
+ Stable v0.2 remains the compatibility boundary. Alpha consumers must migrate
+ to `CommandOccurrence.Arguments` and pattern-match the closed value and
+ redirect families; no aliases or obsolete adapters preserve the old model.
- `PwshParserOptions.Dialect` is additive and defaults to `PowerShell7` for
compatibility. Native Windows consumers select it only for a compatible
PowerShell 7.6 host (`>=7.6.4` and `<7.7`) and select
diff --git a/SPEC.POWERSHELL.md b/SPEC.POWERSHELL.md
index 85c5fc3..ca5d9c4 100644
--- a/SPEC.POWERSHELL.md
+++ b/SPEC.POWERSHELL.md
@@ -1292,14 +1292,16 @@ about which stream produced it:
| stream merge `N>&1` for `N` in `2`–`6`, or `*>&1` | `ErrOut` when `N` is `2`, else `Out`; `Target` carries `&1` verbatim with `IsDynamicSkip=true` |
The table above remains the v0.2 `Redirect` compatibility mapping. v0.3 also
-populates `RedirectAnalysis`: `RedirectSourceKind.PowerShellAllStreams`
-preserves `*`, `Descriptor` preserves numeric streams, and `Operation`
-distinguishes file output/append from static descriptor duplication. Static
-descriptor operations are not path-relevant. PowerShell's grammar does not
+populates the closed `RedirectAnalysis` family:
+`RedirectSource.PowerShellAllStreams` preserves `*`,
+`RedirectSource.Descriptor` preserves numeric streams, and runtime alternatives
+distinguish `FileRedirectAnalysis` output/append from static
+`DescriptorDuplicateRedirectAnalysis`. Descriptor alternatives are not paths.
+PowerShell's grammar does not
admit descriptor close, move, computed merge targets, or file input
redirection; those spellings make the whole parse unparseable. `$null` and
`${null}`
-remains an incomplete explicit redirect until the public operation vocabulary
+remain an incomplete explicit redirect until the public operation vocabulary
has a discard-sink representation; consumers must continue to fail closed.
---
diff --git a/SPEC.md b/SPEC.md
index 661b0e5..df14131 100644
--- a/SPEC.md
+++ b/SPEC.md
@@ -205,16 +205,10 @@ public sealed record CommandListSyntax : ShellSyntaxNode { ... }
public sealed record CommandListItemSyntax { ... }
public sealed record GroupSyntax : ShellSyntaxNode { ... }
public sealed record ForEachSyntax : ShellSyntaxNode { ... }
-public sealed record LoopBindingSyntax { ... }
public sealed record ShellSourceFragment { ... }
-public sealed record ConditionLoopSyntax : ShellSyntaxNode { ... }
-public sealed record ConditionalSyntax : ShellSyntaxNode { ... }
-public sealed record ConditionalBranchSyntax : ShellSyntaxNode { ... }
public sealed record CommandSubstitutionSyntax : ShellSyntaxNode { ... }
public sealed record ExecutionRegionSyntax : ShellSyntaxNode { ... }
-public enum ShellSyntaxKind { ... }
public enum ShellGroupKind { ... }
-public enum ConditionLoopKind { ... }
public enum ExecutionRegionOrigin { ... }
public enum ExecutionRegionPhase { ... }
public enum ExecutionRegionTiming { ... }
@@ -223,23 +217,28 @@ public enum ExecutionRegionCardinality { ... }
// v0.3 authorization and bounded-analysis projections — see §3.
public sealed record CommandOccurrence { ... }
public sealed record CommandAncestryFrame { ... }
-public sealed record EffectiveArgument { ... }
-public sealed record ShellValueDomain { ... }
-public sealed record RedirectAnalysis { ... }
+public sealed record AnalyzedArgument { ... }
+public abstract record ShellValueDomain { ... }
+public abstract record RedirectAnalysis { ... }
public sealed record HereDocumentAnalysis { ... }
-public sealed record RedirectSource { ... }
+public abstract record RedirectSource { ... }
+public sealed record FileRedirectAnalysis : RedirectAnalysis { ... }
+public sealed record UnresolvedRedirectAnalysis : RedirectAnalysis { ... }
+public sealed record DescriptorDuplicateRedirectAnalysis : RedirectAnalysis { ... }
+public sealed record DescriptorMoveRedirectAnalysis : RedirectAnalysis { ... }
+public sealed record DescriptorCloseRedirectAnalysis : RedirectAnalysis { ... }
+public sealed record HereDocumentRedirectAnalysis : RedirectAnalysis { ... }
+public sealed record HereStringRedirectAnalysis : RedirectAnalysis { ... }
public enum CommandOccurrenceRole { ... }
public enum CommandAncestryRegion { ... }
-public enum ShellValueDomainKind { ... }
+public enum FileRedirectMode { ... }
public enum HereDocumentExpansionMode { ... }
-public enum RedirectSourceKind { ... }
-public enum RedirectOperation { ... }
-public static class ShellAnalysisLimits { ... }
```
-`ConditionLoopSyntax`, `ConditionalSyntax`, and `ConditionalBranchSyntax` are
-reserved v0.3 structural vocabulary. The stable-v0.3 parsers do not emit them;
-condition-loop and branch grammar remains fail closed until a later release.
+Stable v0.3 exposes only structural types the parsers can emit. Condition-loop
+and branch grammar remains fail closed and reserves no public type or enum
+member. Every new v0.3 result type is parser-owned; its constructor and result
+setters are not public. The stable v0.2 constructors and setters are unchanged.
That's the entire public API. **Everything else is internal.** The lexer,
parser internals, verb tables, resolver — all implementation detail.
@@ -263,13 +262,13 @@ public sealed record ParsedCommand
/// source ranges; decoded wrapper nodes report unavailable ranges unless
/// an exact outer mapping exists.
///
- public ShellBlockSyntax Syntax { get; init; } = new();
+ public ShellBlockSyntax Syntax { get; internal init; } = new();
///
/// Canonical authorization projection containing every authored simple
/// command that may execute exactly once, in deterministic source order.
///
- public IReadOnlyList Commands { get; init; } = [];
+ public IReadOnlyList Commands { get; internal init; } = [];
///
/// Conservative v0.2 compatibility projection. Existing simple-command
@@ -424,65 +423,55 @@ for structure, explanation, display, and specialized analysis.
public abstract record ShellSyntaxNode
{
private protected ShellSyntaxNode() { }
+ private protected abstract object LibraryOwnership { get; }
- public abstract ShellSyntaxKind Kind { get; }
- public int? SourceStart { get; init; }
- public int? SourceLength { get; init; }
-}
-
-public enum ShellSyntaxKind
-{
- Unknown,
- Block,
- SimpleCommand,
- Pipeline,
- CommandList,
- Group,
- ForEach,
- ConditionLoop,
- Conditional,
- ConditionalBranch,
- CommandSubstitution,
- ExecutionRegion,
+ public int? SourceStart { get; internal init; }
+ public int? SourceLength { get; internal init; }
}
public sealed record ShellBlockSyntax : ShellSyntaxNode
{
- public override ShellSyntaxKind Kind => ShellSyntaxKind.Block;
- public IReadOnlyList Statements { get; init; } = [];
+ internal ShellBlockSyntax() { }
+ private protected override object LibraryOwnership => this;
+ public IReadOnlyList Statements { get; internal init; } = [];
}
public sealed record SimpleCommandSyntax : ShellSyntaxNode
{
- public override ShellSyntaxKind Kind => ShellSyntaxKind.SimpleCommand;
- public Clause Clause { get; init; } = new();
- public IReadOnlyList Substitutions { get; init; } = [];
- public IReadOnlyList ExecutionRegions { get; init; } = [];
+ internal SimpleCommandSyntax() { }
+ private protected override object LibraryOwnership => this;
+ public Clause Clause { get; internal init; } = new();
+ public IReadOnlyList Substitutions { get; internal init; } = [];
+ public IReadOnlyList ExecutionRegions { get; internal init; } = [];
}
public sealed record PipelineSyntax : ShellSyntaxNode
{
- public override ShellSyntaxKind Kind => ShellSyntaxKind.Pipeline;
- public IReadOnlyList Stages { get; init; } = [];
+ internal PipelineSyntax() { }
+ private protected override object LibraryOwnership => this;
+ public IReadOnlyList Stages { get; internal init; } = [];
}
public sealed record CommandListSyntax : ShellSyntaxNode
{
- public override ShellSyntaxKind Kind => ShellSyntaxKind.CommandList;
- public IReadOnlyList Items { get; init; } = [];
+ internal CommandListSyntax() { }
+ private protected override object LibraryOwnership => this;
+ public IReadOnlyList Items { get; internal init; } = [];
}
public sealed record CommandListItemSyntax
{
- public CompoundOperator Operator { get; init; }
- public ShellSyntaxNode Command { get; init; } = new ShellBlockSyntax();
+ internal CommandListItemSyntax() { }
+ public CompoundOperator Operator { get; internal init; }
+ public ShellSyntaxNode Command { get; internal init; } = null!;
}
public sealed record GroupSyntax : ShellSyntaxNode
{
- public override ShellSyntaxKind Kind => ShellSyntaxKind.Group;
- public ShellGroupKind GroupKind { get; init; }
- public ShellBlockSyntax Body { get; init; } = new();
+ internal GroupSyntax() { }
+ private protected override object LibraryOwnership => this;
+ public ShellGroupKind GroupKind { get; internal init; }
+ public ShellBlockSyntax Body { get; internal init; } = null!;
}
public enum ShellGroupKind
@@ -494,70 +483,40 @@ public enum ShellGroupKind
public sealed record ForEachSyntax : ShellSyntaxNode
{
- public override ShellSyntaxKind Kind => ShellSyntaxKind.ForEach;
- public LoopBindingSyntax Binding { get; init; } = new();
- public ShellSourceFragment Iterable { get; init; } = new();
- public ShellBlockSyntax IteratorCommands { get; init; } = new();
- public ShellBlockSyntax Body { get; init; } = new();
-}
-
-public sealed record LoopBindingSyntax
-{
- public string Name { get; init; } = "";
- public ShellSourceFragment Source { get; init; } = new();
+ internal ForEachSyntax() { }
+ private protected override object LibraryOwnership => this;
+ public string BindingName { get; internal init; } = "";
+ public ShellSourceFragment BindingSource { get; internal init; } = null!;
+ public ShellSourceFragment Iterable { get; internal init; } = null!;
+ public ShellBlockSyntax IteratorCommands { get; internal init; } = null!;
+ public ShellBlockSyntax Body { get; internal init; } = null!;
}
public sealed record ShellSourceFragment
{
- public string Raw { get; init; } = "";
- public int? SourceStart { get; init; }
- public int? SourceLength { get; init; }
-}
-
-public sealed record ConditionLoopSyntax : ShellSyntaxNode
-{
- public override ShellSyntaxKind Kind => ShellSyntaxKind.ConditionLoop;
- public ConditionLoopKind LoopKind { get; init; }
- public ShellBlockSyntax Condition { get; init; } = new();
- public ShellBlockSyntax Body { get; init; } = new();
-}
-
-public enum ConditionLoopKind
-{
- Unknown,
- While,
- Until,
-}
-
-public sealed record ConditionalSyntax : ShellSyntaxNode
-{
- public override ShellSyntaxKind Kind => ShellSyntaxKind.Conditional;
- public IReadOnlyList Branches { get; init; } = [];
- public ShellBlockSyntax? Else { get; init; }
-}
-
-public sealed record ConditionalBranchSyntax : ShellSyntaxNode
-{
- public override ShellSyntaxKind Kind => ShellSyntaxKind.ConditionalBranch;
- public ShellBlockSyntax Condition { get; init; } = new();
- public ShellBlockSyntax Body { get; init; } = new();
+ internal ShellSourceFragment() { }
+ public string Raw { get; internal init; } = "";
+ public int? SourceStart { get; internal init; }
+ public int? SourceLength { get; internal init; }
}
public sealed record CommandSubstitutionSyntax : ShellSyntaxNode
{
- public override ShellSyntaxKind Kind => ShellSyntaxKind.CommandSubstitution;
- public ShellBlockSyntax Body { get; init; } = new();
+ internal CommandSubstitutionSyntax() { }
+ private protected override object LibraryOwnership => this;
+ public ShellBlockSyntax Body { get; internal init; } = null!;
}
public sealed record ExecutionRegionSyntax : ShellSyntaxNode
{
- public override ShellSyntaxKind Kind => ShellSyntaxKind.ExecutionRegion;
- public ExecutionRegionOrigin Origin { get; init; }
- public int? HostClauseElementIndex { get; init; }
- public ExecutionRegionPhase Phase { get; init; }
- public ExecutionRegionTiming Timing { get; init; }
- public ExecutionRegionCardinality Cardinality { get; init; }
- public ShellBlockSyntax Body { get; init; } = new();
+ internal ExecutionRegionSyntax() { }
+ private protected override object LibraryOwnership => this;
+ public ExecutionRegionOrigin Origin { get; internal init; }
+ public ClauseElement? HostArgument { get; internal init; }
+ public ExecutionRegionPhase Phase { get; internal init; }
+ public ExecutionRegionTiming Timing { get; internal init; }
+ public ExecutionRegionCardinality Cardinality { get; internal init; }
+ public ShellBlockSyntax Body { get; internal init; } = null!;
}
public enum ExecutionRegionOrigin
@@ -604,21 +563,34 @@ PowerShell expressions share a grammar. Direct-source nodes have exact ranges
into `ParsedCommand.Source`. Nodes lifted from decoded, escaped, or encoded
wrapper content use null ranges unless an exact outer mapping exists.
-Every v0.3 enum reserves zero as `Unknown`. Consumers fail closed on
-`Unknown` or an unrecognized numeric value when the fact affects policy.
+Every new v0.3 result-type constructor and new v0.3 member setter is
+library-owned. Every `IReadOnlyList` introduced by v0.3 is a defensive
+snapshot, not a mutable array or list that a consumer can cast and change.
+Stable v0.2 construction, setters, and list semantics remain unchanged.
+Runtime type is the discriminant for each closed
+record family; consumers keep a default fail-closed switch arm for future
+library-owned alternatives. Compatibility is required against stable v0.2,
+not against any experimental `0.3.0-alpha.*` surface.
+
+Every v0.3 enum whose model admits an unknown state reserves zero as
+`Unknown`. Consumers fail closed on `Unknown` or an unrecognized numeric value
+when the fact affects policy. `FileRedirectMode` has no `Unknown` member:
+unresolved operations use `UnresolvedRedirectAnalysis`, and only the library
+can construct a `FileRedirectAnalysis` with a known mode.
### Command occurrences and bounded values (v0.3)
```csharp
public sealed record CommandOccurrence
{
- public Clause Clause { get; init; } = new();
- public CommandOccurrenceRole ImmediateRole { get; init; }
- public IReadOnlyList Ancestry { get; init; } = [];
- public IReadOnlyList EffectiveArguments { get; init; } = [];
- public ShellValueDomain WorkingDirectory { get; init; } = ShellValueDomain.Unknown;
- public IReadOnlyList Redirects { get; init; } = [];
- public bool IsComplete { get; init; }
+ internal CommandOccurrence() { }
+ public Clause Clause { get; internal init; } = new();
+ public CommandOccurrenceRole ImmediateRole { get; internal init; }
+ public IReadOnlyList Ancestry { get; internal init; } = [];
+ public IReadOnlyList Arguments { get; internal init; } = [];
+ public ShellValueDomain WorkingDirectory { get; internal init; } = null!;
+ public IReadOnlyList Redirects { get; internal init; } = [];
+ public bool IsComplete { get; internal init; }
}
public enum CommandOccurrenceRole
@@ -626,21 +598,18 @@ public enum CommandOccurrenceRole
Unknown,
Ordinary,
PipelineStage,
- Condition,
Iterator,
LoopBody,
- Branch,
Substitution,
ExecutionRegion,
}
public sealed record CommandAncestryFrame
{
- public ShellSyntaxKind AncestorKind { get; init; }
- public CommandAncestryRegion Region { get; init; }
- public int? ChildIndex { get; init; }
- public int? SourceStart { get; init; }
- public int? SourceLength { get; init; }
+ internal CommandAncestryFrame() { }
+ public ShellSyntaxNode Ancestor { get; internal init; } = null!;
+ public CommandAncestryRegion Region { get; internal init; }
+ public int? ChildIndex { get; internal init; }
}
public enum CommandAncestryRegion
@@ -652,41 +621,34 @@ public enum CommandAncestryRegion
GroupBody,
Iterator,
LoopBody,
- Condition,
- Branch,
Substitution,
ExecutionRegion,
}
-public sealed record EffectiveArgument
+public sealed record AnalyzedArgument
{
- public int ClauseElementIndex { get; init; } = -1;
- public ShellValueDomain Value { get; init; } = ShellValueDomain.Unknown;
-}
-
-public sealed record ShellValueDomain
-{
- public static ShellValueDomain Unknown { get; } = new();
-
- public ShellValueDomainKind Kind { get; init; }
- public IReadOnlyList Values { get; init; } = [];
- public string? Pattern { get; init; }
- public string? CoveringDirectory { get; init; }
+ internal AnalyzedArgument() { }
+ public Arg Argument { get; internal init; } = null!;
+ public ClauseElement Element { get; internal init; } = null!;
+ public ShellValueDomain Value { get; internal init; } = null!;
}
-public enum ShellValueDomainKind
+public abstract record ShellValueDomain
{
- Unknown,
- Exact,
- FiniteSet,
- Pattern,
-}
+ private protected ShellValueDomain() { }
+ private protected abstract object LibraryOwnership { get; }
-public static class ShellAnalysisLimits
-{
- public static int MaxValueCandidates => 32;
- public static int MaxStructuralNesting => 16;
- public static int MaxWrapperRecursionDepth => 5;
+ public sealed record Unknown : ShellValueDomain { ... }
+ public sealed record Exact : ShellValueDomain { public string Value { get; } }
+ public sealed record FiniteSet : ShellValueDomain
+ {
+ public IReadOnlyList Values { get; }
+ }
+ public sealed record PathPattern : ShellValueDomain
+ {
+ public string Pattern { get; }
+ public string CoveringDirectory { get; }
+ }
}
```
@@ -717,11 +679,11 @@ that a proved or conservatively unknown command binding may execute. The
unchanged host `Clause` retains its authored script-block `DynamicSkip`
argument. A direct PowerShell `& {}` or `. {}` region appears as a statement,
carries `Origin=DirectCall` or `Origin=DotSource`, has
-`HostClauseElementIndex=null`, and creates no synthetic occurrence for the
+`HostArgument=null`, and creates no synthetic occurrence for the
invocation operator. A command-owned region carries `Origin=CommandArgument`
-and the non-negative index of its script-block token in the host
-`Clause.Elements`. Consumers use `Origin`, rather than reparsing source text,
-to distinguish the different direct-invocation state semantics.
+and `HostArgument` references the exact script-block `ClauseElement` in the
+host command. Consumers use `Origin`, rather than reparsing source text, to
+distinguish the different direct-invocation state semantics.
Execution-region origin, phase, timing, and cardinality are independent
structural facts. Attached regions retain authored script-block order even when a
@@ -745,49 +707,49 @@ zero-based `ChildIndex` in the structural collection that owns the
`CommandSubstitutionSyntax`. For a simple command this is its `Substitutions`
collection; for an iterator it is the containing iterator-command collection.
-Each ancestry frame describes the relationship from its `AncestorKind` to the
-next node on the path. The root block uses `Root`; non-root blocks and command
+Each ancestry frame references the actual `Ancestor` node and describes its
+relationship to the next node on the path. The root block uses `Root`; non-root blocks and command
lists use `Statement`; pipelines use `PipelineStage`; groups use `GroupBody`;
-foreach nodes use `Iterator` or `LoopBody`; condition loops use `Condition` or
-`LoopBody`; conditionals use `Branch`; conditional-branch nodes use
-`Condition` or `Branch`; command substitutions use `Substitution`; and
+foreach nodes use `Iterator` or `LoopBody`; command substitutions use `Substitution`; and
execution regions use `ExecutionRegion`.
-Repeated children use their zero-based authored index. The `else` child uses
-the branch count, placing it after every condition/body pair. Frame source
-ranges belong to the ancestor. Blocks, command lists, and groups retain the
+Repeated children use their zero-based authored index. Source ranges are read
+from the referenced ancestor. Blocks, command lists, and groups retain the
incoming immediate role; pipeline stages, iterator/body regions,
-condition/body regions, branches, substitutions, and execution regions replace
-it with their nearer execution role.
+substitutions, and execution regions replace it with their nearer execution role.
Projection accepts only a parser-owned tree: a syntax-node or `Clause`
reference cannot appear at two authored positions, node and fragment spans are
either both unavailable or a non-negative start/length pair, and structural
enum values consumed by the projector must be known. Empty blocks remain
-valid, but empty pipelines, command lists, and conditionals are malformed.
-Joined value domains, effective-argument coordinates, cwd facts, redirect
+valid, but empty pipelines and command lists are malformed.
+Joined value domains, analyzed-argument references, cwd facts, redirect
coordinates, redirect shapes, and heredoc facts must satisfy their contracts.
Any violation discards the partial `Commands` and `Clauses` collections and
makes the outer parse unparseable; it is never published as a complete
occurrence.
-`EffectiveArgument.ClauseElementIndex` is a stable authored coordinate. The
-analysis never mutates a compatibility `Arg` to hold loop-specific values.
-An occurrence can be structurally complete while one effective value remains
-`Unknown`; completeness and value precision are independent.
+`Arguments` contains exactly one `AnalyzedArgument` for each non-cwd
+`Clause.Args` entry in authored order. `Argument` and `Element` reference the
+existing compatibility objects directly. Inline equals/colon forms may create
+multiple arguments that share one source element. The analysis never mutates a
+compatibility `Arg` to hold loop-specific values. An occurrence can be
+structurally complete while one value remains `Unknown`; completeness and
+value precision are independent.
The parser emits only these value-domain combinations:
-- `Unknown`: no values, pattern, or covering directory;
-- `Exact`: exactly one value and no pattern fields;
-- `FiniteSet`: 2–32 distinct values and no pattern fields;
-- `Pattern`: no values, a non-empty pattern, and a non-empty covering directory.
+- `Unknown`: no payload;
+- `Exact`: exactly one non-null value;
+- `FiniteSet`: 2–32 distinct non-null values;
+- `PathPattern`: a non-empty pattern and non-empty covering directory.
The parser does not execute commands, inspect runtime variables, enumerate the
filesystem, or truncate an over-limit set and call it complete. A result with
-33 or more candidates becomes `Unknown`. Structural depth starts at zero for
-the root and increments on foreach loops, condition loops, conditionals,
-groups, command substitutions, and execution regions; blocks, lists,
-pipelines, branches, and
+33 or more candidates becomes `Unknown`. The candidate cap of 32, structural
+depth cap of 16, and wrapper-recursion cap of 5 are parser contracts, not
+public tuning knobs. Structural depth starts at zero for the root and
+increments on foreach loops, groups, command substitutions, and execution
+regions; blocks, lists, pipelines, and
simple-command leaves do not independently increment it. Exceeding 16
structural containers or 5 decoded-command wrapper recursions makes the entire
result unparseable.
@@ -943,76 +905,98 @@ Occurrence-specific redirect analysis is additive. The existing `Redirect`
record remains the v0.2 compatibility leaf and is not reinterpreted.
```csharp
-public sealed record RedirectAnalysis
+public abstract record RedirectSource
{
- public int RedirectIndex { get; init; } = -1;
- public RedirectSource Source { get; init; } = new();
- public RedirectOperation Operation { get; init; }
- public int? TargetDescriptor { get; init; }
- public ShellValueDomain Target { get; init; } = ShellValueDomain.Unknown;
- public HereDocumentAnalysis? HereDocument { get; init; }
- public bool IsPathRelevant { get; init; }
- public bool IsComplete { get; init; }
+ private protected RedirectSource() { }
+ private protected abstract object LibraryOwnership { get; }
+
+ public sealed record Unknown : RedirectSource { ... }
+ public sealed record Default : RedirectSource { ... }
+ public sealed record Descriptor : RedirectSource { public int Value { get; } }
+ public sealed record PowerShellAllStreams : RedirectSource { ... }
}
-public sealed record HereDocumentAnalysis
+public abstract record RedirectAnalysis
{
- public ShellSourceFragment Delimiter { get; init; } = new();
- public ShellSourceFragment Body { get; init; } = new();
- public HereDocumentExpansionMode ExpansionMode { get; init; }
- public bool StripLeadingTabs { get; init; }
- public bool IsComplete { get; init; }
+ private protected RedirectAnalysis() { }
+ private protected abstract object LibraryOwnership { get; }
+
+ public Redirect Authored { get; internal init; } = null!;
+ public RedirectSource Source { get; internal init; } = null!;
+ public bool IsComplete { get; internal init; }
}
-public enum HereDocumentExpansionMode
+public sealed record FileRedirectAnalysis : RedirectAnalysis
{
- Unknown,
- Literal,
- Expand,
+ public FileRedirectMode Mode { get; }
+ public ShellValueDomain Target { get; internal init; } = null!;
}
-public sealed record RedirectSource
+public sealed record UnresolvedRedirectAnalysis : RedirectAnalysis { ... }
+public sealed record DescriptorDuplicateRedirectAnalysis : RedirectAnalysis
{
- public RedirectSourceKind Kind { get; init; }
- public int? Descriptor { get; init; }
+ public int TargetDescriptor { get; internal init; }
}
-
-public enum RedirectSourceKind
+public sealed record DescriptorMoveRedirectAnalysis : RedirectAnalysis
{
- Unknown,
- Default,
- Descriptor,
- PowerShellAllStreams,
+ public int TargetDescriptor { get; internal init; }
+}
+public sealed record DescriptorCloseRedirectAnalysis : RedirectAnalysis { ... }
+public sealed record HereDocumentRedirectAnalysis : RedirectAnalysis
+{
+ public HereDocumentAnalysis Document { get; internal init; } = null!;
+}
+public sealed record HereStringRedirectAnalysis : RedirectAnalysis
+{
+ public ShellValueDomain Data { get; internal init; } = null!;
}
-public enum RedirectOperation
+public enum FileRedirectMode
{
- Unknown,
- FileInput,
- FileOutput,
- FileAppend,
- DescriptorDuplicate,
- DescriptorClose,
- DescriptorMove,
+ Input,
+ Output,
+ Append,
CombinedOutput,
CombinedOutputAppend,
- HereDocument,
- HereString,
}
+
+public sealed record HereDocumentAnalysis
+{
+ internal HereDocumentAnalysis() { }
+ public ShellSourceFragment Delimiter { get; internal init; } = null!;
+ public ShellSourceFragment Body { get; internal init; } = null!;
+ public HereDocumentExpansionMode ExpansionMode { get; internal init; }
+ public bool StripLeadingTabs { get; internal init; }
+ public bool IsComplete { get; internal init; }
+}
+
+public enum HereDocumentExpansionMode
+{
+ Unknown,
+ Literal,
+ Expand,
+}
+
```
-`RedirectIndex` correlates to `Clause.Redirects`. `RedirectSource` preserves a
-shell-default stream, an explicit numeric descriptor, or PowerShell's `*`
-selector without erasing shell identity. Invalid source-kind/descriptor
-combinations are incomplete and fail closed. File targets are path-relevant;
-static descriptor duplicate, close, and move operations are not paths.
-
-`HereDocument` is non-null only for `HereDocument`. A quoted delimiter makes
-the body `Literal`; an expanding body is complete only when every supported
-execution-bearing substitution has been discovered as its own command
-occurrence. Bash `HereString` data uses `Target`, includes the shell's trailing
-newline in an exact value, and is not path-relevant. PowerShell here-strings
-remain ordinary value tokens rather than redirect operations.
+`Authored` references the exact corresponding `Clause.Redirects` leaf.
+`RedirectSource` preserves a shell-default stream, an explicit numeric
+descriptor, or PowerShell's `*` selector without erasing shell identity. File
+alternatives are path-relevant; descriptor alternatives are not paths.
+
+A quoted heredoc delimiter makes the body `Literal`; an expanding body is
+complete only when every supported execution-bearing substitution has been
+discovered as its own command occurrence. Bash `HereString` data uses `Data`,
+includes the shell's trailing newline in an exact value, and is not
+path-relevant. PowerShell here-strings remain ordinary value tokens.
+
+`RedirectSource.Unknown` is valid only with `UnresolvedRedirectAnalysis`.
+`RedirectSource.Default` may own ordinary or combined file, descriptor,
+heredoc, or here-string alternatives.
+`RedirectSource.Descriptor` may own ordinary file, descriptor, heredoc, or
+here-string alternatives but never combined-output. PowerShell all-streams may
+own only output/append file alternatives or descriptor duplication to stream
+1. Any other internal pairing discards the authorization projection.
A Bash file redirect whose expansion cannot prove exactly one target has an
`Unknown` target and `IsComplete=false`; its containing command occurrence is
diff --git a/docs/CONSUMER_GUIDE.md b/docs/CONSUMER_GUIDE.md
index e646206..6666505 100644
--- a/docs/CONSUMER_GUIDE.md
+++ b/docs/CONSUMER_GUIDE.md
@@ -129,8 +129,8 @@ Once parsed, a security-oriented consumer normally follows this sequence:
2. Walk every command occurrence; do not authorize only the first stage of a
compound, pipeline, loop, substitution, or execution region.
3. Determine a conservative command identity.
-4. Overlay effective values on authored elements, then evaluate working-
- directory and redirect facts.
+4. Evaluate the already-joined arguments, working-directory domain, and
+ redirect alternatives.
5. Elevate dynamic or unresolved content when it affects the policy decision.
6. Apply product-specific rules and produce a decision.
@@ -152,31 +152,22 @@ var commandDecision = GateDecision.Allow();
foreach (var occurrence in parsed.Commands)
{
+ var gateKey = GetGateKey(occurrence.Clause.Verb);
GateDecision occurrenceDecision;
if (!occurrence.IsComplete
|| !IsKnownRole(occurrence.ImmediateRole)
- || occurrence.Clause.Verb.IsDynamic)
+ || gateKey is null)
{
occurrenceDecision = GateDecision.Prompt(
"command execution is not statically bounded");
}
else
{
- var gateKey = GetGateKey(occurrence.Clause.Verb);
- if (gateKey is null)
- {
- occurrenceDecision = GateDecision.Prompt(
- "occurrence has no statically known command");
- }
- else
- {
- // This application-owned step must interpret the authored
- // Clause.Elements, overlay EffectiveArguments by
- // ClauseElementIndex, and apply the complete grammar for gateKey
- // to every exact or finite candidate.
- occurrenceDecision = EvaluateOccurrence(gateKey, occurrence);
- }
+ // Arguments already joins each non-cwd Arg to its source element and
+ // effective value. Apply the complete grammar for gateKey to every
+ // exact or finite candidate.
+ occurrenceDecision = EvaluateOccurrence(gateKey, occurrence);
}
// Do not return early on Prompt: a later occurrence may be Deny.
@@ -190,10 +181,8 @@ return commandDecision;
static bool IsKnownRole(CommandOccurrenceRole role) => role is
CommandOccurrenceRole.Ordinary
or CommandOccurrenceRole.PipelineStage
- or CommandOccurrenceRole.Condition
or CommandOccurrenceRole.Iterator
or CommandOccurrenceRole.LoopBody
- or CommandOccurrenceRole.Branch
or CommandOccurrenceRole.Substitution
or CommandOccurrenceRole.ExecutionRegion;
```
@@ -209,29 +198,49 @@ approval must never bypass a deny.
## v0.3 authorization and migration contract
-The `0.3.0-alpha.1` package adds `ParsedCommand.Commands` as the authorization
-projection and `ParsedCommand.Syntax` as the typed display/analysis tree. This
-guide describes the stable v0.3 contract; constructs not yet complete in an
-installed prerelease remain prompt-or-deny cases. The migration rules are:
+`ParsedCommand.Commands` is the authorization projection and
+`ParsedCommand.Syntax` is the typed display/analysis tree. The stable v0.2
+surface remains available: `Source`, `Clauses`, `IsUnparseable`, and
+`UnparseableReason` retain their existing contract throughout v0.3.
+
+The stable v0.3 migration rules are:
1. Check `IsUnparseable` first. An unparseable result has empty `Commands` and
`Clauses`; any partial `Syntax` is diagnostic only.
-2. Authorize every `CommandOccurrence`, including iterator, condition, branch,
- substitution, and loop-body commands. Do not recursively walk `Syntax` to
- discover commands.
+2. Authorize every `CommandOccurrence`, including iterator, loop-body,
+ substitution, and execution-region commands. Do not recursively walk
+ `Syntax` to discover commands.
3. Require `CommandOccurrence.IsComplete`, a recognized `ImmediateRole`, and a
static command identity before considering approval reuse.
4. Preserve authored PowerShell parameter/argument classification, then apply
shell binding and executable-specific grammar to every exact or finite
effective value. A value that begins with `-` can affect a native command;
it does not retroactively become a PowerShell cmdlet parameter token.
-5. Evaluate every redirect through its explicit operation, source, target,
- path relevance, and completeness. Do not infer descriptor safety from raw
- prefixes.
+5. Evaluate every redirect through its closed runtime alternative, source,
+ value, and completeness. Do not infer descriptor safety from raw prefixes.
6. Prompt or deny when an unknown value can affect identity, options, path
scope, cwd, or redirects. A structurally complete occurrence may still have
an unknown value; those are separate facts.
+The prerelease `0.3.0-alpha.*` surface was experimental and has no
+compatibility promise. Consumers moving from an alpha must make these source
+changes:
+
+| Prerelease shape | Stable v0.3 shape |
+|---|---|
+| `EffectiveArguments` plus `ClauseElementIndex` | `Arguments`, with direct `Argument` and `Element` references |
+| `ShellValueDomain.Kind`, `Values`, `Pattern` | pattern-match `Unknown`, `Exact`, `FiniteSet`, or `PathPattern` |
+| `RedirectAnalysis.Operation` property bag | pattern-match the redirect record alternative |
+| `RedirectSource.Kind` plus `Descriptor` | pattern-match `Default`, `Descriptor`, `PowerShellAllStreams`, or `Unknown` |
+| copied ancestry kind/span fields | `CommandAncestryFrame.Ancestor` plus `Region` and `ChildIndex` |
+| `ExecutionRegionSyntax.HostClauseElementIndex` | `HostArgument`, the actual `ClauseElement` |
+| `ShellSyntaxKind` | pattern-match the runtime syntax-node type |
+| public condition/branch nodes and roles | removed because no parser published them |
+
+There are intentionally no obsolete aliases or adapters for those alpha
+shapes. Recompile against the selected package and fix every use; this avoids
+silently preserving an obsolete property-bag policy interpretation.
+
This authorization is about authored shell syntax. `IsComplete` means the
parser found and classified every executable region in the submitted command;
it does not promise which runtime executable an ambient alias, function,
@@ -339,23 +348,26 @@ release; no removal version is scheduled. A later removal would require a
deliberate minor-version breaking change and release-note migration mapping
under the repository's `0.x` versioning contract.
-The new records participate in generated record equality, hashing, and
-`ToString()`. Adding `Syntax` and `Commands` also changes those generated
+All new v0.3 result-type constructors and member setters are parser-owned, and
+all lists introduced by v0.3 are defensive read-only snapshots. Stable v0.2
+construction and list semantics remain unchanged. The records participate in generated
+record equality, hashing, and `ToString()`. Adding `Syntax` and `Commands` also changes those generated
results for `ParsedCommand`, even when the compatibility `Clauses` are equal.
Do not use a parser result's record hash or `ToString()` as a durable approval
key. ShellSyntaxTree does not promise a stable serialized wire format for its
closed polymorphic syntax family and does not configure polymorphic JSON
serialization. Consumers that persist results should map them to a
-consumer-owned, versioned DTO and reject unknown enum values or node kinds
-when reading it.
+consumer-owned, versioned DTO and reject unknown enum values or runtime
+alternatives when reading it.
## Display traversal is not authorization traversal
`ParsedCommand.Syntax` preserves authored nesting for explainers, diagnostics,
and visualizations. A display can recursively visit `ShellBlockSyntax`,
`PipelineSyntax`, `ForEachSyntax`, `CommandSubstitutionSyntax`,
-`ExecutionRegionSyntax`, and the other known node types. It must include a
-default branch for a node type or `ShellSyntaxKind` added by a future package.
+`ExecutionRegionSyntax`, and the other known node types through runtime type
+patterns. It must include a default branch for a node type added by a future
+package; stable v0.3 deliberately has no redundant public `ShellSyntaxKind`.
Do not use that recursive display walk to build an authorization list. The
library has already projected every supported executable leaf exactly once
@@ -366,37 +378,168 @@ types can omit executable regions. If `IsUnparseable` is true, any partial
## Interpreting occurrence analysis
-`EffectiveArguments` overlays bounded runtime values onto authored
-`Clause.Elements` by `ClauseElementIndex`; it does not replace the authored
-token or its shell classification. Validate each coordinate before use and
-apply the executable's complete argument grammar to every candidate:
+Each `AnalyzedArgument` directly joins one authored `Arg`, its source
+`ClauseElement`, and its effective `ShellValueDomain`. There is exactly one
+entry for every non-cwd `Clause.Args` entry, in authored order. Attached forms
+such as `--work-tree=../repo` and `-Path:C:\repo` can produce two `Arg`
+records that share one source element; consumers do not need to reconstruct
+that normal many-to-one relationship from indexes or source spans.
+
+The examples in this guide use a compact result notation rather than dumping
+the complete object graph. Each one shows the submitted input, the
+policy-relevant facts returned by the parser, and the decision those facts
+enable. Names such as `Exact("/work")` and `Descriptor(2)` denote the
+corresponding closed runtime alternatives, not strings that consumers need to
+parse.
+
+For example, parse this with `BashParser`, `WorkingDirectory = "/work"`:
+
+```bash
+cat file.txt | grep x && rm /tmp/stale
+```
+
+The authorization projection is:
+
+| `Commands` index | Authored command | `ImmediateRole` | `IsComplete` | `WorkingDirectory` |
+|---:|---|---|---|---|
+| 0 | `cat file.txt` | `PipelineStage` | `true` | `Exact("/work")` |
+| 1 | `grep x` | `PipelineStage` | `true` | `Exact("/work")` |
+| 2 | `rm /tmp/stale` | `Ordinary` | `true` | `Exact("/work")` |
+
+The consumer evaluates all three rows. It may group the first two into one
+pipeline-shaped prompt for display, but that grouping does not authorize the
+second stage implicitly. The final `rm` occurrence is also evaluated even if
+an earlier occurrence already requires a prompt, because it may produce a
+hard deny.
+
+Attached option forms demonstrate why `AnalyzedArgument` includes direct
+object references. For this Bash input:
+
+```bash
+git --work-tree=../repo status
+```
+
+`Commands[0].Arguments` contains three entries:
+
+| `Argument.Raw` | `Value` | `Element.Raw` |
+|---|---|---|
+| `--work-tree` | `Exact("--work-tree")` | `--work-tree=../repo` |
+| `../repo` | `Exact("../repo")` | `--work-tree=../repo` |
+| `status` | `Exact("status")` | `status` |
+
+The first two entries reference the same `ClauseElement`. A consumer can bind
+the option and its operand without source-span arithmetic or re-tokenizing the
+command. PowerShell attached parameters such as
+`Remove-Item -Path:C:\repo` use the same many-to-one shape.
+
+Apply the executable's complete argument grammar to every value:
+
+```csharp
+foreach (var analyzed in occurrence.Arguments)
+{
+ var current = analyzed.Value switch
+ {
+ ShellValueDomain.Exact exact =>
+ EvaluateArgument(analyzed.Argument, analyzed.Element, exact.Value),
+ ShellValueDomain.FiniteSet finite =>
+ EvaluateEveryCandidate(
+ analyzed.Argument,
+ analyzed.Element,
+ finite.Values),
+ ShellValueDomain.PathPattern pattern =>
+ EvaluatePattern(pattern.Pattern, pattern.CoveringDirectory),
+ ShellValueDomain.Unknown =>
+ GateDecision.Prompt("policy-sensitive argument is unknown"),
+ _ => GateDecision.Prompt("unrecognized value-domain alternative"),
+ };
+
+ decision = MostRestrictive(decision, current);
+}
+```
- `Exact` contains one proved value.
- `FiniteSet` contains 2 through 32 distinct proved values. Every candidate
must independently satisfy policy; do not authorize only the first.
-- `Pattern` is a Bash path-shaped glob plus a conservative
+- `PathPattern` is a Bash path-shaped glob plus a conservative
`CoveringDirectory`. Accept it only when policy understands both the pattern
and the full covering scope without enumerating the filesystem.
- `Unknown` is not an empty string or wildcard grant. Prompt or deny whenever
the value can affect identity, option binding, a path, or another
policy-sensitive position.
-Only the combinations documented above are valid. An empty `Exact`, a
-one-value `FiniteSet`, populated `Values` on `Unknown`, or an unrecognized
-`ShellValueDomainKind` is invalid external data and must fail closed.
+Use runtime type patterns rather than a parallel kind enum. Keep a default
+prompt-or-deny branch so a future library-owned alternative cannot be treated
+as safe accidentally.
`WorkingDirectory` uses the same domain type, but stable v0.3 publishes only
`Exact` or `Unknown`. `Exact` means all modeled reachable states agree. A
-branch, loop, failed location change, or unmodeled mutation whose exits do not
-agree produces `Unknown`; never substitute the process cwd as a fallback.
+branch, zero-or-more loop, failed location change, or unmodeled mutation whose
+exits do not agree produces `Unknown`; never substitute the process cwd as a
+fallback.
+
+Redirect analysis is independent. Require `RedirectAnalysis.IsComplete` and
+pattern-match its closed runtime alternative. File alternatives carry path
+domains; descriptor alternatives are not paths; heredoc and here-string
+alternatives carry stdin data whose meaning remains receiver-specific. An
+occurrence can be complete while an argument, cwd, or redirect value is
+unknown, so test all facts separately.
+
+## Evaluating loops
+
+A loop body is represented once as authored syntax. ShellSyntaxTree does not
+pretend that it executed the loop or duplicate a command occurrence for every
+candidate value. Instead, it gives the loop-dependent argument a value domain.
+
+With `BashInitialStateMode.IsolatedNonInteractive` and
+`WorkingDirectory = "/work"`, this input:
+
+```bash
+for f in a.txt b.txt; do rm -- "$f"; done
+```
+
+produces one loop-body occurrence:
+
+```text
+Commands[0]
+ Clause.Verb.Tokens: ["rm"]
+ ImmediateRole: LoopBody
+ IsComplete: true
+ WorkingDirectory: Exact("/work")
+ Arguments[0]: "--" -> Exact("--")
+ Arguments[1]: "\"$f\"" -> FiniteSet("a.txt", "b.txt")
+```
+
+The consumer applies the complete `rm` grammar and path policy to both
+`a.txt` and `b.txt`. It must not approve only the first candidate, and it must
+not mistake one occurrence for proof that the command runs only once.
+
+PowerShell uses the same consumer shape. Under
+`PwshInitialStateMode.IsolatedNonInteractiveNoProfile`, this input:
-Redirect analysis is independent. Require `RedirectAnalysis.IsComplete`, a
-recognized source and operation, valid descriptor combinations, and a value
-domain appropriate to the operation. Evaluate every path-relevant target.
-Descriptor operations are not paths, while heredoc and here-string targets are
-stdin data; executable-specific policy still decides whether that data is
-code. An occurrence can be structurally complete while one argument, cwd, or
-redirect target remains unknown, so test all of these facts separately.
+```powershell
+foreach ($f in @('a.txt', 'b.txt', 'a.txt')) { Write-Output $F }
+```
+
+produces one `LoopBody` occurrence whose `$F` argument is
+`FiniteSet("a.txt", "b.txt")`; PowerShell's case-insensitive variable binding
+and duplicate elimination have already been reflected in the domain.
+
+The isolated modes are executor assertions, not parser optimizations. With
+the safe default initial-state modes, these ambient-variable-dependent proofs
+remain unknown or make the construct unparseable as specified earlier. A
+consumer must not select an isolated mode merely to obtain a finite set.
+
+Loops also affect later state even when their body facts are static. With an
+incoming cwd of `/work`:
+
+```bash
+for f in /tmp/*.txt; do cd /tmp; done; pwd
+```
+
+the loop may execute zero times, so both the body `cd` occurrence and the
+later `pwd` occurrence report `WorkingDirectory = Unknown`. The reachable
+states are `/work` and `/tmp`; the parser does not choose whichever value
+would make policy easiest. A cwd-sensitive consumer prompts or denies.
## Choosing a command identity
@@ -523,6 +666,19 @@ For `cd /repo && cat file.txt`, the `cat` clause receives a synthetic
directory. PowerShell provides the same contract for `Set-Location` and its
aliases.
+With an incoming cwd of `/work`, the relevant output is:
+
+| Occurrence | `WorkingDirectory` | Authored path | `Arg.Resolved` |
+|---|---|---|---|
+| `cd /repo` | `Exact("/work")` | `/repo` | `/repo` |
+| `cat file.txt` | `Exact("/repo")` | `file.txt` | `/repo/file.txt` |
+
+The `cd` row reports the directory in which `cd` itself runs; the `cat` row
+reports the successful `AndIf` continuation state. This is why consumers
+should use the occurrence's `WorkingDirectory` for execution context and the
+argument's `Resolved` value for path-zone policy rather than trying to infer
+either from clause order.
+
The attributed argument is derived context:
- use it when evaluating where a clause operates;
@@ -543,6 +699,18 @@ write outside an allowed zone:
echo safe > /etc/profile.d/example.sh
```
+With a Bash working directory of `/work`, representative results are:
+
+| Input | Redirect alternative | Source | Relevant value | Complete? | Consumer consequence |
+|---|---|---|---|---:|---|
+| `echo safe > /etc/profile.d/example.sh` | `FileRedirectAnalysis` with `Mode = Output` | `Default` | `Target = Exact("/etc/profile.d/example.sh")` | yes | Apply write-path policy to the exact target. |
+| `command 2>&1` | `DescriptorDuplicateRedirectAnalysis` | `Descriptor(2)` | `TargetDescriptor = 1` | yes | Apply descriptor policy; do not treat `1` as a path. |
+| `command 2>&$FD` | `UnresolvedRedirectAnalysis` | `Unknown` | no proved target descriptor | no | Prompt or deny the occurrence. |
+
+Those are runtime alternatives, not interpretations of a string prefix. In
+particular, the incomplete third row cannot accidentally pass a rule written
+for ordinary stderr-to-stdout duplication.
+
For a v0.2 compatibility consumer, walk `Clause.Redirects` independently of
`Args`:
@@ -571,69 +739,46 @@ var redirectDecision = GateDecision.Allow();
foreach (var redirect in occurrence.Redirects)
{
- GateDecision current;
-
- if (!redirect.IsComplete
- || !IsKnownRedirectSource(redirect.Source)
- || !IsKnownRedirectOperation(redirect.Operation))
- {
- current = GateDecision.Prompt("redirect analysis is incomplete");
- }
- else if (!redirect.IsPathRelevant)
- {
- if (redirect.Operation is RedirectOperation.HereDocument
- or RedirectOperation.HereString)
- {
- // Preserve Target and HereDocument for receiver-specific stdin
- // policy; this data may be code for the receiving executable.
- current = EvaluateStdinData(occurrence, redirect);
- }
- else
- {
- current = EvaluateDescriptorOperation(
- redirect.Source,
- redirect.Operation,
- redirect.TargetDescriptor);
- }
- }
- else if (redirect.Target.Kind is not (
- ShellValueDomainKind.Exact or ShellValueDomainKind.FiniteSet))
- {
- current = GateDecision.Prompt("redirect path is unknown");
- }
- else
- {
- current = GateDecision.Allow();
- foreach (var path in redirect.Target.Values)
+ var current = !redirect.IsComplete || !IsKnownSource(redirect.Source)
+ ? GateDecision.Prompt("redirect analysis is incomplete")
+ : redirect switch
{
- current = MostRestrictive(current, EvaluatePath(path));
- }
- }
+ FileRedirectAnalysis file => EvaluateFileRedirect(file),
+ DescriptorDuplicateRedirectAnalysis duplicate =>
+ EvaluateDescriptorDuplicate(redirect.Source, duplicate.TargetDescriptor),
+ DescriptorMoveRedirectAnalysis move =>
+ EvaluateDescriptorMove(redirect.Source, move.TargetDescriptor),
+ DescriptorCloseRedirectAnalysis =>
+ EvaluateDescriptorClose(redirect.Source),
+ HereDocumentRedirectAnalysis heredoc =>
+ EvaluateStdinData(occurrence, heredoc.Document),
+ HereStringRedirectAnalysis hereString =>
+ EvaluateStdinData(occurrence, hereString.Data),
+ UnresolvedRedirectAnalysis =>
+ GateDecision.Prompt("redirect operation is unresolved"),
+ _ => GateDecision.Prompt("unrecognized redirect alternative"),
+ };
redirectDecision = MostRestrictive(redirectDecision, current);
}
return redirectDecision;
-static bool IsKnownRedirectSource(RedirectSource source) => source.Kind switch
-{
- RedirectSourceKind.Default => source.Descriptor is null,
- RedirectSourceKind.Descriptor => source.Descriptor >= 0,
- RedirectSourceKind.PowerShellAllStreams => source.Descriptor is null,
- _ => false,
-};
-
-static bool IsKnownRedirectOperation(RedirectOperation operation) =>
- operation is RedirectOperation.FileInput
- or RedirectOperation.FileOutput
- or RedirectOperation.FileAppend
- or RedirectOperation.DescriptorDuplicate
- or RedirectOperation.DescriptorClose
- or RedirectOperation.DescriptorMove
- or RedirectOperation.CombinedOutput
- or RedirectOperation.CombinedOutputAppend
- or RedirectOperation.HereDocument
- or RedirectOperation.HereString;
+static bool IsKnownSource(RedirectSource source) => source is
+ RedirectSource.Default
+ or RedirectSource.Descriptor
+ or RedirectSource.PowerShellAllStreams;
+
+static GateDecision EvaluateFileRedirect(FileRedirectAnalysis redirect) =>
+ redirect.Target switch
+ {
+ ShellValueDomain.Exact exact => EvaluatePath(exact.Value),
+ ShellValueDomain.FiniteSet finite => EvaluateEveryPath(finite.Values),
+ ShellValueDomain.PathPattern pattern =>
+ EvaluatePattern(pattern.Pattern, pattern.CoveringDirectory),
+ ShellValueDomain.Unknown => GateDecision.Prompt("redirect path is unknown"),
+ _ => GateDecision.Prompt("unrecognized redirect target alternative"),
+ };
```
Completeness and value precision are intentionally independent. For example,
@@ -642,7 +787,7 @@ target, so path policy still prompts. Under an isolated fresh-process
PowerShell initial state,
`foreach ($f in @('one.txt','two.txt')) {
Write-Output x > $f }` can instead expose a finite set of two absolute target
-paths. The loop target is not added to `EffectiveArguments`, because a
+paths. The loop target is not added to `Arguments`, because a
redirect operand is not part of the command's argv.
PowerShell stream facts retain numbered sources and the all-streams selector:
@@ -698,6 +843,24 @@ invocation. By contrast, `& $(Write-Output Get-Date)` also retains an
incomplete dynamic outer occurrence because PowerShell invokes the produced
name.
+Bash exposes the same execution-before-container ordering. For:
+
+```bash
+rm "$(find /tmp)"
+```
+
+the relevant projection is:
+
+| `Commands` index | Command | `ImmediateRole` | Argument value |
+|---:|---|---|---|
+| 0 | `find /tmp` | `Substitution` | `/tmp` is `Exact("/tmp")` |
+| 1 | `rm "$(find /tmp)"` | `Ordinary` | produced filename is `Unknown` |
+
+The `find` occurrence is independently authorizable, but its presence does not
+make the bytes it prints a statically known `rm` operand. A path-sensitive
+policy therefore evaluates `find` and still prompts or denies `rm`. It does not
+walk `Syntax` afterward and authorize `find` a second time.
+
Quoting also determines the scope of host-wrapper substitutions. In
`pwsh -Command "Write-Output $(Get-Date)"`, the parent evaluates `Get-Date`, so
the result contains that parent-scope occurrence plus an incomplete outer
@@ -734,6 +897,18 @@ The recoverable outcome is normally a user prompt with a one-time option, or a
deny. A false-negative approval match causes another prompt; a false-positive
match can silently execute something the operator did not authorize.
+Two different result shapes reach that same safe outcome:
+
+| Input and parser | Relevant output | Why reusable approval stops |
+|---|---|---|
+| PowerShell: `& $exe` | `IsUnparseable = false`; one occurrence with `IsComplete = false` and `Verb.IsDynamic = true` | The syntax is recognized, but the executable identity is not bounded. |
+| Bash: `if true; then echo ok; fi` | `IsUnparseable = true`; `Commands` and `Clauses` are empty | The unsupported control construct may contain execution, so partial syntax is diagnostic only. |
+
+`IsUnparseable = false` is therefore not an allow signal. It means only that
+the whole input was not rejected as an unsupported or unsafe-to-project
+construct; the consumer still checks every occurrence and every
+policy-sensitive domain.
+
## Worked use cases
### AI-agent approval gate
diff --git a/openspec/changes/v0-3-structured-shell-analysis/design.md b/openspec/changes/v0-3-structured-shell-analysis/design.md
index 94bede1..70be56a 100644
--- a/openspec/changes/v0-3-structured-shell-analysis/design.md
+++ b/openspec/changes/v0-3-structured-shell-analysis/design.md
@@ -1123,9 +1123,11 @@ constructed by the library and exposes read-only facts to consumers. Internal
constructors and `internal init` accessors are shown to make ownership explicit
but are not consumer-callable API.
-Every exposed `IReadOnlyList` is backed by a library-owned immutable
-collection or a defensive copy. The parser never publishes a mutable array or
-list that a consumer can cast and change after projection.
+Every `IReadOnlyList` introduced by v0.3 is backed by a library-owned
+immutable collection or a defensive copy. The parser never publishes a mutable
+array or list for a new v0.3 member that a consumer can cast and change after
+projection. Stable v0.2 construction, setters, and list semantics are
+unchanged.
Runtime type is the discriminant for each closed family. Consumers use an
exhaustive type switch with a default fail-closed arm. The library may add a
diff --git a/openspec/changes/v0-3-structured-shell-analysis/tasks.md b/openspec/changes/v0-3-structured-shell-analysis/tasks.md
index 67bad72..9354f9c 100644
--- a/openspec/changes/v0-3-structured-shell-analysis/tasks.md
+++ b/openspec/changes/v0-3-structured-shell-analysis/tasks.md
@@ -29,7 +29,7 @@
`PwshDialect` contract. Preserve PowerShell 7 as the compatibility default,
make Windows PowerShell 5.1 explicit, keep Bash and PowerShell top-level
parsing separate, and require dialect-local grammar/catalog proof.
-- [ ] 1.14 Correct the unreleased v0.3 consumer API before stable release.
+- [x] 1.14 Correct the unreleased v0.3 consumer API before stable release.
Preserve every stable v0.2 type and the `Syntax` / `Commands` / `Clauses`
lanes, but do not preserve alpha-only v0.3 members. Replace sparse effective-
argument coordinates with one joined analyzed argument per authored non-cwd
@@ -37,11 +37,12 @@
closed parser-owned alternatives; reference actual syntax ancestors; remove
redundant kind/source copies and public condition/branch types that stable
v0.3 never emits; keep executable policy consumer-owned.
-- [ ] 1.15 Synchronize the corrected API into `SPEC.md`, implementation,
+- [x] 1.15 Synchronize the corrected API into `SPEC.md`, implementation,
public-API snapshots, corpus DTOs, README, and consumer guide in one vertical
slice. Add prerelease migration notes for alpha consumers without an adapter
or obsolete compatibility layer. Defensively copy or immutably back every
- published `IReadOnlyList` and pin many-to-one inline-option argument joins.
+ `IReadOnlyList` introduced by v0.3 and pin many-to-one inline-option
+ argument joins without changing stable v0.2 list semantics.
- [ ] 1.16 Migrate Netclaw from alpha.5 to the corrected prerelease, delete its
coordinate/property-bag validation, and rerun the full Linux plus native-
Windows approval matrices before stable v0.3.
@@ -71,7 +72,7 @@
- [x] 3.10b Implement the bounded expanding-heredoc slice with quote-removed delimiters, literal quoted/escaped bodies, tab stripping, exact provenance, and fail-closed unsupported header/body forms.
- [x] 3.10c Require proved Bash variable-attribute state for simple named-parameter dereferences and fail closed globally on the locked unmodeled execution-bearing builtin catalog, including exact dispatch-wrapper bypasses.
- [x] 3.10d Fail closed globally on Bash `exec`, mutating or ambiguous `hash`, alias, `shopt`, and `enable` forms, and unmodeled reserved execution prefixes/groups while retaining only exact static query grammar.
-- [ ] 3.11 Implement PowerShell `$()` discovery in supported words, redirect values, foreach expressions, call-operator dynamic identities, standalone expression statements, double-quoted strings, and expandable here-strings; never invent invocation from standalone output, retain literal/escaped spellings, and fail closed on trailing command-style arguments, call-operator script blocks, or unsupported execution-bearing `@()` / `@{}` forms.
+- [x] 3.11 Implement PowerShell `$()` discovery in supported words, redirect values, foreach expressions, call-operator dynamic identities, standalone expression statements, double-quoted strings, and expandable here-strings; never invent invocation from standalone output, retain literal/escaped spellings, and fail closed on trailing command-style arguments, call-operator script blocks, or unsupported execution-bearing `@()` / `@{}` forms.
- [x] 3.11a Implement words, redirect values, call-operator dynamic identities, standalone statements, expandable strings/here-strings, and parent-versus-child host payload provenance; fail closed on arbitrary expression values and unsupported execution-bearing `@()` / `@{}` forms.
- [x] 3.12 Pin substitution parentage, authored sibling indices, innermost-first ordering, Bash-isolated versus PowerShell-current-scope state, unknown-state propagation, nesting/depth limits, and incomplete dynamic identities in direct tests.
- [x] 3.12a Pin the Bash argument/redirect slice, isolated cwd behavior, wrapper provenance, and the shared structural-depth budget.
@@ -83,8 +84,8 @@
- [x] 3.13a Promote the Bash ordinary, multiple, nested, redirect, quoted, escaped, stateful, malformed, and hidden-execution cases into its executable corpus.
- [x] 3.13b Promote the PowerShell ordinary, multiple, nested, redirect, quoted, escaped, stateful, malformed, expression-boundary, and hidden-execution cases into its executable corpus.
- [x] 3.13c Promote expanding, literal, tab-stripped, multiple, and malformed Bash heredoc cases with full structural expectations into the executable corpus.
- - [ ] 3.13d Promote sanitized nameref, unknown-state dereference, and execution-bearing builtin failures into the Bash executable corpus and Netclaw strict matrix.
- - [ ] 3.13e Promote sanitized Bash hash, alias/option, exec, builtin-enable, and reserved execution syntax failures into the executable corpus and Netclaw strict matrix.
+ - [x] 3.13d Promote sanitized nameref, unknown-state dereference, and execution-bearing builtin failures into the Bash executable corpus and Netclaw strict matrix.
+ - [x] 3.13e Promote sanitized Bash hash, alias/option, exec, builtin-enable, and reserved execution syntax failures into the executable corpus and Netclaw strict matrix.
- [x] 3.14 Add `ExecutionRegionSyntax`, its four discriminant enums,
`SimpleCommandSyntax.ExecutionRegions`, and appended occurrence/ancestry enum
members to the public API and snapshot without changing existing enum values.
@@ -146,7 +147,7 @@
- [x] 6.2 Emit condition-free loop-body occurrences and conservative compatibility clauses.
- [x] 6.3 Derive exact and finite literal binding domains within the locked candidate cap.
- [x] 6.4 Substitute a bounded binding only where Bash quoting proves argument boundaries.
-- [ ] 6.5 Propagate and conservatively join cwd and supported binding state across zero-or-more loop execution.
+- [x] 6.5 Propagate and conservatively join cwd and supported binding state across zero-or-more loop execution.
- [x] 6.5a Lock outcome-partitioned Bash flow, failure-aware `cd`,
conservative `lastpipe` / `pipefail`, ordered iteration plans,
decoded-wrapper inheritance, and dynamic fail-closed compatibility
@@ -154,7 +155,7 @@
- [x] 6.5b Apply outcome-sensitive cwd analysis to existing Bash lists,
pipelines, substitutions, subshells, and decoded wrappers; rebase exact
compatibility paths and retain `` after conservative joins.
- - [ ] 6.5c Carry ordered loop binding and cwd state through zero-or-more
+ - [x] 6.5c Carry ordered loop binding and cwd state through zero-or-more
iterations, then remove the temporary loop-mutation rejection.
- [x] 6.5c.1 Correct the contract after adversarial review: require an
explicit isolated initial-state mode and supported scalar-name boundary;
@@ -187,7 +188,11 @@
- The executable corpus includes indirect and parameter-operator rejection,
loop-body substitution, and atomic transition-budget overflow; native
oracles pin the shell semantics behind the conservative boundaries.
-- [ ] 6.8 Add sanitized Bash corpus entries and Netclaw allow/prompt/deny integration cases.
+- [ ] 6.8 Add sanitized Bash corpus entries and Netclaw disposition cases.
+ Under Netclaw's enforced `BashInitialStateMode.Unknown` contract, bounded
+ loop cases must remain prompt or deny; require an allow case only if the
+ executor later proves the full isolated non-interactive startup/environment
+ contract rather than selecting that parser mode solely to reduce prompts.
## 7. PowerShell Foreach Vertical Slice
@@ -255,13 +260,13 @@
sanitizes unknown joins. Broader wrapper,
pipeline, alias/cmdlet/native, redirect, and adversarial matrices remain in
tasks 7.5-7.7.
-- [ ] 7.5 Cover aliases, cmdlets, native commands, nested loops, pipelines, script blocks, and wrapper boundaries.
+- [x] 7.5 Cover aliases, cmdlets, native commands, nested loops, pipelines, script blocks, and wrapper boundaries.
- [x] 7.5a Implement the version-pinned PowerShell 7 script-block receiver and
parameter-binding catalog, including aliases, supported module-qualified
identities, parameter abbreviations/inline values, positional binding,
parameter sets, `ScriptBlock[]`, and ForEach-Object Begin/Process/End
assignment.
- - [ ] 7.5b Implement direct `& {}` and `. {}` plus synchronous current-runspace
+ - [x] 7.5b Implement direct `& {}` and `. {}` plus synchronous current-runspace
regions for ForEach-Object, Where-Object, Measure-Command, Trace-Command,
in-process Invoke-Command, and New-Module with shell-specific state flow.
- [x] 7.5c Implement Start-Job and initialization, ForEach-Object -Parallel,
@@ -283,7 +288,7 @@
proved data, an unknown receiver, proved local `Invoke-Command`, the exact
module-qualified-looking mutation boundary, and canonical-target
invalidation through `echo`.
- - [ ] 7.5f Pin authored projection order separately from semantic phase order,
+ - [x] 7.5f Pin authored projection order separately from semantic phase order,
exact host element coordinates, nested regions, wrappers, pipelines, loops,
and the 16-container depth boundary.
- [x] 7.5g Retain explicit atomic-failure behavior for direct-block arguments
@@ -327,7 +332,7 @@
passed on Ubuntu and Windows. Both jobs used the hash-pinned PowerShell
7.6.4 oracle; the Windows job additionally discovered native Windows
PowerShell 5.1 from Bash and passed all 2,815 tests plus package creation.
-- [ ] 7.10 Migrate Netclaw's native Windows environment to prefer a compatible
+- [x] 7.10 Migrate Netclaw's native Windows environment to prefer a compatible
`pwsh.exe`, fall back to `powershell.exe`, and carry one canonical platform,
executable, and dialect identity through LLM context, parser, approval
policy, and executor. Reparse and reauthorize if fallback selection changes.
@@ -348,23 +353,26 @@
## 11. Verification and Release
-- [ ] 11.1 Replace the alpha public-API default-value, equality, serialization,
+- [x] 11.1 Replace the alpha public-API default-value, equality, serialization,
immutability, and unknown-case tests with the corrected stable-v0.3 contract.
- - Alpha evidence: `V03PublicApiSnapshotTests` currently pins every additive record default and enum
- zero value, proves `Syntax` and `Commands` participate in generated record
- equality and `ToString()` plus equal-record hash consistency, demonstrates
- that default JSON is not a polymorphic round-trip contract, and makes every
- policy-sensitive unknown numeric enum value detectable for consumer rejection.
-- [ ] 11.2 Assert every supported executable region appears exactly once and every unsupported executable region makes the result unparseable.
-- [ ] 11.3 Run the complete Bash and PowerShell corpus suites plus the PII audit against the corrected implementation.
- - Alpha evidence: the complete suites and PII audit pass on the superseded alpha surface.
-- [ ] 11.4 Run `dotnet build -c Release`, `dotnet test -c Release`, `dotnet pack -c Release`, and header verification against the corrected implementation.
- - Alpha evidence: these gates pass on the superseded alpha implementation.
-- [ ] 11.5 Validate the corrected public API field-for-field against the synchronized shared and PowerShell specifications.
- - Alpha evidence: `PublicApiSnapshotTests` and `V03PublicApiSnapshotTests` currently enumerate the exact
- exported namespace, type family, exact property sets, parser constructors
- and entry points, enum ordering, reference nullability, defaults, and fixed
- limits synchronized into `SPEC.md` and `SPEC.POWERSHELL.md`.
+ - Corrected evidence: `V03PublicApiSnapshotTests` pins the closed record
+ families, parser-owned construction, defensive lists, generated
+ `ParsedCommand` equality/hash/`ToString()` behavior, absence of a default
+ polymorphic JSON round trip, and detectable future enum values.
+- [x] 11.2 Assert every supported executable region appears exactly once and every unsupported executable region makes the result unparseable.
+- [x] 11.3 Run the complete Bash and PowerShell corpus suites plus the PII audit against the corrected implementation.
+ - Corrected evidence: the complete Release suite passes 2,870/2,870 and the
+ explicit PII audit passes 1/1, including Bash corpus case 309.
+- [x] 11.4 Run `dotnet build -c Release`, `dotnet test -c Release`, `dotnet pack -c Release`, and header verification against the corrected implementation.
+ - Corrected evidence: Release build passes with zero warnings, all 2,870
+ tests pass, package and symbols pack successfully, and header verification
+ reports every file compliant.
+- [x] 11.5 Validate the corrected public API field-for-field against the synchronized shared and PowerShell specifications.
+ - Corrected evidence: `PublicApiSnapshotTests` and
+ `V03PublicApiSnapshotTests` enumerate the exact exported namespace, stable
+ v0.2 members, corrected v0.3 type families, property accessors, parser
+ entry points, enum ordering, and nullability synchronized into `SPEC.md`
+ and `SPEC.POWERSHELL.md`.
- [ ] 11.6 Validate Netclaw's ordinary-command, redirect, bounded-loop, and unknown-value approval matrices against the prerelease package.
- [x] 11.7 Update release notes and remove Netclaw's temporary descriptor workaround only after explicit redirect integration is live.
- The `0.3.0-alpha` release notes document the explicit redirect model. The
diff --git a/src/ShellSyntaxTree/CommandOccurrence.cs b/src/ShellSyntaxTree/CommandOccurrence.cs
index 9853123..d07bed6 100644
--- a/src/ShellSyntaxTree/CommandOccurrence.cs
+++ b/src/ShellSyntaxTree/CommandOccurrence.cs
@@ -5,147 +5,279 @@
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
+using ShellSyntaxTree.Internal;
namespace ShellSyntaxTree;
/// One authored simple command that may execute.
public sealed record CommandOccurrence
{
+ private IReadOnlyList _ancestry =
+ Array.Empty();
+ private IReadOnlyList _arguments = Array.Empty();
+ private IReadOnlyList _redirects = Array.Empty();
+
+ internal CommandOccurrence()
+ {
+ }
+
/// Gets the shared compatibility leaf.
- public Clause Clause { get; init; } = new();
+ public Clause Clause { get; internal init; } = new();
/// Gets the nearest structural execution role.
- public CommandOccurrenceRole ImmediateRole { get; init; }
+ public CommandOccurrenceRole ImmediateRole { get; internal init; }
/// Gets ancestry ordered from outermost to innermost.
- public IReadOnlyList Ancestry { get; init; } =
- Array.Empty();
+ public IReadOnlyList Ancestry
+ {
+ get => _ancestry;
+ internal init => _ancestry = PublicCollection.Copy(value);
+ }
+
+ /// Gets one analyzed value for every authored, non-cwd argument.
+ public IReadOnlyList Arguments
+ {
+ get => _arguments;
+ internal init => _arguments = PublicCollection.Copy(value);
+ }
+
+ internal IReadOnlyList EffectiveArguments
+ {
+ get
+ {
+ var values = new List();
+ for (var index = 0; index < _arguments.Count; index++)
+ {
+ if (!_arguments[index].HasEffectiveValue)
+ {
+ continue;
+ }
+
+ values.Add(new EffectiveArgument
+ {
+ ClauseElementIndex = FindElementIndex(_arguments[index].Element),
+ Value = _arguments[index].Value,
+ });
+ }
- /// Gets bounded effective values at authored argument coordinates.
- public IReadOnlyList EffectiveArguments { get; init; } =
- Array.Empty();
+ return values.ToArray();
+ }
+ }
/// Gets the effective working-directory proof.
- public ShellValueDomain WorkingDirectory { get; init; } = ShellValueDomain.Unknown;
+ public ShellValueDomain WorkingDirectory { get; internal init; } =
+ new ShellValueDomain.Unknown();
/// Gets explicit redirect analysis in compatibility redirect order.
- public IReadOnlyList Redirects { get; init; } =
- Array.Empty();
-
- ///
- /// Gets whether command identity, ancestry, and parser-owned shell analysis
- /// are structurally complete.
- ///
- public bool IsComplete { get; init; }
+ public IReadOnlyList Redirects
+ {
+ get => _redirects;
+ internal init => _redirects = PublicCollection.Copy(value);
+ }
+
+ /// Gets whether parser-owned shell analysis is structurally complete.
+ public bool IsComplete { get; internal init; }
+
+ private int FindElementIndex(ClauseElement element)
+ {
+ for (var index = 0; index < Clause.Elements.Count; index++)
+ {
+ if (ReferenceEquals(Clause.Elements[index], element))
+ {
+ return index;
+ }
+ }
+
+ return -1;
+ }
}
/// Identifies the nearest structural role of a command occurrence.
public enum CommandOccurrenceRole
{
- /// The role is unknown.
Unknown,
- /// An ordinary command.
Ordinary,
- /// A pipeline stage.
PipelineStage,
- /// A condition command.
- Condition,
- /// An iterator-producing command.
Iterator,
- /// A loop-body command.
LoopBody,
- /// A conditional-branch command.
- Branch,
- /// A substitution command.
Substitution,
- /// A command inside an execution-bearing region.
ExecutionRegion,
}
/// One compositional structural ancestor of a command occurrence.
public sealed record CommandAncestryFrame
{
- /// Gets the ancestor's syntax kind.
- public ShellSyntaxKind AncestorKind { get; init; }
+ internal CommandAncestryFrame()
+ {
+ }
+
+ /// Gets the actual ancestor node.
+ public ShellSyntaxNode Ancestor { get; internal init; } = null!;
/// Gets the occurrence's region within the ancestor.
- public CommandAncestryRegion Region { get; init; }
+ public CommandAncestryRegion Region { get; internal init; }
/// Gets the child index for repeated regions, when applicable.
- public int? ChildIndex { get; init; }
+ public int? ChildIndex { get; internal init; }
- /// Gets the ancestor source start, when exact.
- public int? SourceStart { get; init; }
+ internal ShellSyntaxKind AncestorKind => Ancestor.Kind;
- /// Gets the ancestor source length, when exact.
- public int? SourceLength { get; init; }
+ internal int? SourceStart => Ancestor.SourceStart;
+
+ internal int? SourceLength => Ancestor.SourceLength;
}
/// Identifies an occurrence's region within a structural ancestor.
public enum CommandAncestryRegion
{
- /// The region is unknown.
Unknown,
- /// The root region.
Root,
- /// An ordinary statement region.
Statement,
- /// A pipeline-stage region.
PipelineStage,
- /// A group body.
GroupBody,
- /// An iterator expression.
Iterator,
- /// A loop body.
LoopBody,
- /// A condition region.
- Condition,
- /// A conditional branch.
- Branch,
- /// A command substitution.
Substitution,
- /// An execution-bearing region.
ExecutionRegion,
}
-/// A bounded effective value at one authored clause-element coordinate.
-public sealed record EffectiveArgument
+/// One parser-owned join between a compatibility argument and its source.
+public sealed record AnalyzedArgument
+{
+ internal AnalyzedArgument()
+ {
+ }
+
+ /// Gets the compatibility argument.
+ public Arg Argument { get; internal init; } = null!;
+
+ /// Gets the authored element that produced the argument.
+ public ClauseElement Element { get; internal init; } = null!;
+
+ /// Gets the effective shell-value proof.
+ public ShellValueDomain Value { get; internal init; } = null!;
+
+ internal bool HasEffectiveValue { get; init; }
+}
+
+internal sealed record EffectiveArgument
{
- /// Gets the index into .
- public int ClauseElementIndex { get; init; } = -1;
+ internal int ClauseElementIndex { get; init; } = -1;
- /// Gets the effective value proof.
- public ShellValueDomain Value { get; init; } = ShellValueDomain.Unknown;
+ internal ShellValueDomain Value { get; init; } = null!;
}
/// A bounded, non-executing proof for a shell value.
-public sealed record ShellValueDomain
+public abstract record ShellValueDomain
{
- /// Gets the shared empty unknown-domain value.
- public static ShellValueDomain Unknown { get; } = new();
+ private protected ShellValueDomain()
+ {
+ }
+
+ private protected abstract object LibraryOwnership { get; }
+
+ internal abstract ShellValueDomainKind InternalKind { get; }
+
+ internal ShellValueDomainKind Kind => InternalKind;
+
+ internal virtual IReadOnlyList InternalValues => Array.Empty();
+
+ internal IReadOnlyList Values => InternalValues;
+
+ internal virtual string? InternalPattern => null;
+
+ internal string? Pattern => InternalPattern;
+
+ internal virtual string? InternalCoveringDirectory => null;
+
+ internal string? CoveringDirectory => InternalCoveringDirectory;
- /// Gets the proof kind.
- public ShellValueDomainKind Kind { get; init; }
+ /// No bounded value is proved.
+ public sealed record Unknown : ShellValueDomain
+ {
+ internal Unknown()
+ {
+ }
+
+ private protected override object LibraryOwnership => this;
+
+ internal override ShellValueDomainKind InternalKind => ShellValueDomainKind.Unknown;
+ }
+
+ /// Exactly one value is proved.
+ public sealed record Exact : ShellValueDomain
+ {
+ internal Exact(string value) => Value = value;
+
+ private protected override object LibraryOwnership => this;
+
+ internal override ShellValueDomainKind InternalKind => ShellValueDomainKind.Exact;
+
+ internal override IReadOnlyList InternalValues => new[] { Value };
+
+ /// Gets the proved value.
+ public string Value { get; }
+ }
+
+ /// Two through 32 distinct values are proved.
+ public sealed record FiniteSet : ShellValueDomain
+ {
+ internal FiniteSet(IEnumerable values) =>
+ Values = PublicCollection.Copy(values);
+
+ private protected override object LibraryOwnership => this;
+
+ internal override ShellValueDomainKind InternalKind =>
+ ShellValueDomainKind.FiniteSet;
+
+ internal override IReadOnlyList InternalValues => Values;
- /// Gets exact or finite values.
- public IReadOnlyList Values { get; init; } = Array.Empty();
+ /// Gets the proved finite values.
+ public new IReadOnlyList Values { get; }
+ }
- /// Gets a bounded symbolic pattern.
- public string? Pattern { get; init; }
+ /// A bounded path pattern and its conservative covering directory.
+ public sealed record PathPattern : ShellValueDomain
+ {
+ internal PathPattern(string pattern, string coveringDirectory)
+ {
+ Pattern = pattern;
+ CoveringDirectory = coveringDirectory;
+ }
- /// Gets the pattern's conservative covering directory.
- public string? CoveringDirectory { get; init; }
+ private protected override object LibraryOwnership => this;
+
+ internal override ShellValueDomainKind InternalKind => ShellValueDomainKind.Pattern;
+
+ internal override string InternalPattern => Pattern;
+
+ internal override string InternalCoveringDirectory => CoveringDirectory;
+
+ /// Gets the symbolic path pattern.
+ public new string Pattern { get; }
+
+ /// Gets the conservative covering directory.
+ public new string CoveringDirectory { get; }
+ }
}
-/// Identifies the strength and shape of a shell-value proof.
-public enum ShellValueDomainKind
+internal enum ShellValueDomainKind
{
- /// No bounded value is proved.
Unknown,
- /// Exactly one value is proved.
Exact,
- /// Two through 32 distinct values are proved.
FiniteSet,
- /// A pattern and conservative covering directory are proved.
Pattern,
}
+
+internal static class ShellValueDomains
+{
+ internal static ShellValueDomain Unknown { get; } = new ShellValueDomain.Unknown();
+
+ internal static ShellValueDomain Exact(string value) => new ShellValueDomain.Exact(value);
+
+ internal static ShellValueDomain FiniteSet(IEnumerable values) =>
+ new ShellValueDomain.FiniteSet(values);
+
+ internal static ShellValueDomain Pattern(string pattern, string coveringDirectory) =>
+ new ShellValueDomain.PathPattern(pattern, coveringDirectory);
+}
diff --git a/src/ShellSyntaxTree/Internal/Bash/Parsing/BashAbstractStateAnalyzer.cs b/src/ShellSyntaxTree/Internal/Bash/Parsing/BashAbstractStateAnalyzer.cs
index 562d3b8..a06db50 100644
--- a/src/ShellSyntaxTree/Internal/Bash/Parsing/BashAbstractStateAnalyzer.cs
+++ b/src/ShellSyntaxTree/Internal/Bash/Parsing/BashAbstractStateAnalyzer.cs
@@ -25,7 +25,7 @@ internal sealed class BashAbstractStateAnalyzer
private readonly Func _forInPlanFactory;
private readonly Dictionary _inputs =
new(ClauseReferenceComparer.Instance);
- private readonly Dictionary>
+ private readonly Dictionary>
_effectiveArguments = new(ClauseReferenceComparer.Instance);
private readonly Dictionary> _cwdResolutionSanitization =
new(ClauseReferenceComparer.Instance);
@@ -464,7 +464,7 @@ private void RecordEffectiveArguments(
return;
}
- Dictionary? accumulated = null;
+ Dictionary? accumulated = null;
var evaluator = input.Bindings;
foreach (var provenance in sourceFacts.ValueProvenance)
{
@@ -548,7 +548,7 @@ private static bool RequiresIndependentEffectiveValue(
private bool TryAnalyzeParserKnownValue(
ShellValue value,
- out ShellValueDomain domain)
+ out ShellValueDomainFacts domain)
{
var homeDirectory = BashResolver.GetHomeDirectory(_options);
var composed = new StringBuilder(value.Decoded.Length);
@@ -566,7 +566,7 @@ private bool TryAnalyzeParserKnownValue(
if (fragment.Kind != ShellValueFragmentKind.Expansion ||
fragment.Expansion is not ShellExpansionReference expansion)
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return false;
}
@@ -589,7 +589,7 @@ private bool TryAnalyzeParserKnownValue(
if (tildeKind == BashTildeExpansionKind.Unknown ||
homeDirectory.Length == 0)
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return false;
}
@@ -605,14 +605,14 @@ private bool TryAnalyzeParserKnownValue(
((fragment.AllowedTransforms & ShellLexicalTransform.FieldSplit) != 0 &&
ContainsFieldSplitOrGlobCharacter(homeDirectory)))
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return false;
}
composed.Append(homeDirectory);
}
- domain = new ShellValueDomain
+ domain = new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { composed.ToString() },
@@ -677,7 +677,7 @@ private void RecordUnvisitedBindingArguments(
bindingName))
{
GetEffectiveArguments(simple.Clause)[provenance.ClauseElementIndex] =
- ShellValueDomain.Unknown;
+ ShellValueDomainFacts.Unknown;
}
}
@@ -737,14 +737,14 @@ private void RecordUnvisitedBindingArguments(
}
}
- private Dictionary GetEffectiveArguments(Clause clause)
+ private Dictionary GetEffectiveArguments(Clause clause)
{
if (_effectiveArguments.TryGetValue(clause, out var accumulated))
{
return accumulated;
}
- accumulated = new Dictionary();
+ accumulated = new Dictionary();
_effectiveArguments.Add(clause, accumulated);
return accumulated;
}
@@ -902,7 +902,7 @@ private void BuildEffectiveCwdArguments(
if (domain.Kind != ShellValueDomainKind.Exact &&
TryResolveKnownHomeWord(argumentValues[index], input, out var homeWord))
{
- domain = new ShellValueDomain
+ domain = new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { homeWord },
@@ -1396,20 +1396,20 @@ private SimpleCommandSyntax RewriteSimple(
};
}
- private IReadOnlyList CreateEffectiveArguments(Clause clause)
+ private IReadOnlyList CreateEffectiveArguments(Clause clause)
{
if (!_effectiveArguments.TryGetValue(clause, out var accumulated) ||
accumulated.Count == 0)
{
- return Array.Empty();
+ return Array.Empty();
}
var indices = new List(accumulated.Keys);
indices.Sort();
- var effective = new EffectiveArgument[indices.Count];
+ var effective = new EffectiveArgumentFacts[indices.Count];
for (var index = 0; index < effective.Length; index++)
{
- effective[index] = new EffectiveArgument
+ effective[index] = new EffectiveArgumentFacts
{
ClauseElementIndex = indices[index],
Value = accumulated[indices[index]],
@@ -1605,8 +1605,8 @@ private IReadOnlyList RewriteCompatibilityRedirects(
return redirects;
}
- private static IReadOnlyList RewriteRedirectFacts(
- IReadOnlyList source,
+ private static IReadOnlyList RewriteRedirectFacts(
+ IReadOnlyList source,
IReadOnlyList provenance,
BashLoopBindingContext bindings,
Clause clause)
@@ -1616,7 +1616,7 @@ private static IReadOnlyList RewriteRedirectFacts(
return source;
}
- var rewritten = new RedirectAnalysis[source.Count];
+ var rewritten = new RedirectAnalysisFacts[source.Count];
for (var index = 0; index < rewritten.Length; index++)
{
var fact = source[index];
@@ -1644,8 +1644,8 @@ private static IReadOnlyList RewriteRedirectFacts(
rewritten[index] = fact with
{
Target = redirect.IsDynamicSkip
- ? ShellValueDomain.Unknown
- : new ShellValueDomain
+ ? ShellValueDomainFacts.Unknown
+ : new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { redirect.Target },
@@ -1657,8 +1657,8 @@ private static IReadOnlyList RewriteRedirectFacts(
return rewritten;
}
- private static ShellValueDomain RewriteHereStringTarget(
- RedirectAnalysis fact,
+ private static ShellValueDomainFacts RewriteHereStringTarget(
+ RedirectAnalysisFacts fact,
IReadOnlyList provenance,
BashLoopBindingContext bindings)
{
@@ -1677,7 +1677,7 @@ private static ShellValueDomain RewriteHereStringTarget(
if (domain.Kind is not (
ShellValueDomainKind.Exact or ShellValueDomainKind.FiniteSet))
{
- return ShellValueDomain.Unknown;
+ return ShellValueDomainFacts.Unknown;
}
var values = new string[domain.Values.Count];
@@ -1686,7 +1686,7 @@ private static ShellValueDomain RewriteHereStringTarget(
values[index] = domain.Values[index] + "\n";
}
- return new ShellValueDomain
+ return new ShellValueDomainFacts
{
Kind = domain.Kind,
Values = values,
@@ -1696,7 +1696,7 @@ private static ShellValueDomain RewriteHereStringTarget(
return fact.Target;
}
- private static bool AreRedirectsComplete(IReadOnlyList redirects)
+ private static bool AreRedirectsComplete(IReadOnlyList redirects)
{
foreach (var redirect in redirects)
{
@@ -2128,7 +2128,7 @@ internal BashAbstractState(
internal BashAbstractState WithBinding(
string name,
- ShellValueDomain domain) =>
+ ShellValueDomainFacts domain) =>
new(
WorkingDirectory,
HasCompatibilityAttribution,
@@ -2145,10 +2145,10 @@ internal BashAbstractState WithCwd(
internal BashAbstractState WithoutCompatibilityAttribution() =>
new(WorkingDirectory, WorkingDirectory is null, Bindings);
- internal ShellValueDomain ToDomain() =>
+ internal ShellValueDomainFacts ToDomain() =>
WorkingDirectory is null
- ? ShellValueDomain.Unknown
- : new ShellValueDomain
+ ? ShellValueDomainFacts.Unknown
+ : new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { WorkingDirectory },
diff --git a/src/ShellSyntaxTree/Internal/Bash/Parsing/BashCommandParser.cs b/src/ShellSyntaxTree/Internal/Bash/Parsing/BashCommandParser.cs
index 0148f90..1be23d2 100644
--- a/src/ShellSyntaxTree/Internal/Bash/Parsing/BashCommandParser.cs
+++ b/src/ShellSyntaxTree/Internal/Bash/Parsing/BashCommandParser.cs
@@ -501,11 +501,16 @@ private readonly record struct RedirectTargetProvenanceSet(
Clause Clause,
IReadOnlyList Provenance);
+ private readonly record struct RedirectAnalysisSet(
+ Clause Clause,
+ IReadOnlyList Redirects);
+
private readonly record struct BashParseResult(
ParsedCommand Command,
IReadOnlyList CwdPathDependencySets,
IReadOnlyList ValueProvenanceSets,
IReadOnlyList RedirectTargetProvenanceSets,
+ IReadOnlyList RedirectAnalysisSets,
IReadOnlyList ForInPlans);
private static ClauseResult ParseClauseSegment(
diff --git a/src/ShellSyntaxTree/Internal/Bash/Parsing/BashLoopAnalysis.cs b/src/ShellSyntaxTree/Internal/Bash/Parsing/BashLoopAnalysis.cs
index e2f6ce5..6fb8311 100644
--- a/src/ShellSyntaxTree/Internal/Bash/Parsing/BashLoopAnalysis.cs
+++ b/src/ShellSyntaxTree/Internal/Bash/Parsing/BashLoopAnalysis.cs
@@ -38,10 +38,10 @@ internal sealed record BashForInAnalysisPlanReference(
BashForInAnalysisPlan Plan);
internal sealed record BashIterationPlan(
- IReadOnlyList OrderedCandidates,
+ IReadOnlyList OrderedCandidates,
BashIterationCardinality Cardinality,
bool RequiresFixedPoint,
- ShellValueDomain Summary);
+ ShellValueDomainFacts Summary);
///
/// Preserves bounded loop-variable proofs while the Bash structural parser
@@ -55,7 +55,7 @@ internal sealed class BashLoopBindingContext
internal BashLoopBindingContext WithBinding(
string name,
- ShellValueDomain domain)
+ ShellValueDomainFacts domain)
{
var clone = Clone();
for (var index = clone._bindings.Count - 1; index >= 0; index--)
@@ -116,7 +116,7 @@ internal BashBindingAlternativeResult EnumerateExactAlternatives(
{
next.Add(state.WithBinding(
binding.Name,
- new ShellValueDomain
+ new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { value },
@@ -218,7 +218,7 @@ internal static BashLoopBindingContext JoinState(
joined._bindings.Add(new BindingFrame(
name,
leftBinding is null || rightBinding is null
- ? ShellValueDomain.Unknown
+ ? ShellValueDomainFacts.Unknown
: JoinDomains(leftBinding.Domain, rightBinding.Domain)));
}
@@ -251,7 +251,7 @@ leftBinding is not null &&
rightBinding is not null &&
DomainEquals(leftBinding.Domain, rightBinding.Domain)
? leftBinding.Domain
- : ShellValueDomain.Unknown));
+ : ShellValueDomainFacts.Unknown));
}
return widened;
@@ -281,13 +281,13 @@ internal BashIterationPlan AnalyzeIterationPlan(
if (words.Count == 0)
{
return new BashIterationPlan(
- Array.Empty(),
+ Array.Empty(),
BashIterationCardinality.Never,
RequiresFixedPoint: false,
- ShellValueDomain.Unknown);
+ ShellValueDomainFacts.Unknown);
}
- var ordered = new List(words.Count);
+ var ordered = new List(words.Count);
var summary = new List(words.Count);
var distinct = new HashSet(StringComparer.Ordinal);
foreach (var word in words)
@@ -296,7 +296,7 @@ internal BashIterationPlan AnalyzeIterationPlan(
{
return FixedPointPlan(
BashIterationCardinality.ZeroOrMore,
- ShellValueDomain.Unknown);
+ ShellValueDomainFacts.Unknown);
}
if (TryBuildStaticPattern(
@@ -308,10 +308,10 @@ internal BashIterationPlan AnalyzeIterationPlan(
return FixedPointPlan(BashIterationCardinality.ZeroOrMore, pattern);
}
- ShellValueDomain domain;
+ ShellValueDomainFacts domain;
if (IsEntirelyLiteral(word.Value))
{
- domain = new ShellValueDomain
+ domain = new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { word.Value.Decoded },
@@ -336,7 +336,7 @@ internal BashIterationPlan AnalyzeIterationPlan(
{
return FixedPointPlan(
BashIterationCardinality.OneOrMore,
- ShellValueDomain.Unknown);
+ ShellValueDomainFacts.Unknown);
}
}
@@ -364,15 +364,15 @@ internal static BashForInAnalysisPlan CapturePlan(
private static BashIterationPlan FixedPointPlan(
BashIterationCardinality cardinality,
- ShellValueDomain summary) => new(
- Array.Empty(),
+ ShellValueDomainFacts summary) => new(
+ Array.Empty(),
cardinality,
RequiresFixedPoint: true,
summary);
- internal static ShellValueDomain JoinDomains(
- ShellValueDomain left,
- ShellValueDomain right)
+ internal static ShellValueDomainFacts JoinDomains(
+ ShellValueDomainFacts left,
+ ShellValueDomainFacts right)
{
if (DomainEquals(left, right))
{
@@ -384,7 +384,7 @@ internal static ShellValueDomain JoinDomains(
right.Kind is not (
ShellValueDomainKind.Exact or ShellValueDomainKind.FiniteSet))
{
- return ShellValueDomain.Unknown;
+ return ShellValueDomainFacts.Unknown;
}
var values = new List(left.Values.Count + right.Values.Count);
@@ -403,7 +403,7 @@ right.Kind is not (
{
if (distinct.Count > ShellAnalysisLimits.MaxValueCandidates)
{
- return ShellValueDomain.Unknown;
+ return ShellValueDomainFacts.Unknown;
}
values.Add(candidate);
@@ -414,8 +414,8 @@ right.Kind is not (
}
private static bool DomainEquals(
- ShellValueDomain left,
- ShellValueDomain right)
+ ShellValueDomainFacts left,
+ ShellValueDomainFacts right)
{
if (left.Kind != right.Kind ||
!string.Equals(left.Pattern, right.Pattern, StringComparison.Ordinal) ||
@@ -441,7 +441,7 @@ private static bool DomainEquals(
internal bool TryAnalyzeEffectiveValue(
ShellValue value,
- out ShellValueDomain domain)
+ out ShellValueDomainFacts domain)
{
var referenced = new List();
var dependentButUnsupported = false;
@@ -478,13 +478,13 @@ internal bool TryAnalyzeEffectiveValue(
if (referenced.Count == 0 && !dependentButUnsupported)
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return false;
}
if (dependentButUnsupported || ContainsUnresolvedFragment(value, referenced))
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return true;
}
@@ -493,7 +493,7 @@ internal bool TryAnalyzeEffectiveValue(
{
domain = IsOneBindingExpansion(value, referenced[0])
? referenced[0].Domain
- : ShellValueDomain.Unknown;
+ : ShellValueDomainFacts.Unknown;
return true;
}
@@ -502,7 +502,7 @@ internal bool TryAnalyzeEffectiveValue(
if (binding.Domain.Kind is not (
ShellValueDomainKind.Exact or ShellValueDomainKind.FiniteSet))
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return true;
}
}
@@ -518,7 +518,7 @@ internal bool TryAnalyzeEffectiveValue(
candidates,
distinct))
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return true;
}
@@ -526,11 +526,11 @@ internal bool TryAnalyzeEffectiveValue(
return true;
}
- internal ShellValueDomain AnalyzeWordForTransfer(ShellValue value)
+ internal ShellValueDomainFacts AnalyzeWordForTransfer(ShellValue value)
{
if (IsEntirelyLiteral(value))
{
- return new ShellValueDomain
+ return new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { value.Decoded },
@@ -539,7 +539,7 @@ internal ShellValueDomain AnalyzeWordForTransfer(ShellValue value)
return TryAnalyzeEffectiveValue(value, out var domain)
? domain
- : ShellValueDomain.Unknown;
+ : ShellValueDomainFacts.Unknown;
}
private bool TryComposeCandidates(
@@ -751,7 +751,7 @@ private static bool TryBuildStaticPattern(
ShellValue value,
BashParserOptions options,
bool workingDirectoryUnknown,
- out ShellValueDomain pattern)
+ out ShellValueDomainFacts pattern)
{
var containsGlob = false;
foreach (var fragment in value.Fragments)
@@ -765,7 +765,7 @@ private static bool TryBuildStaticPattern(
fragment.Expansion is null ||
fragment.Expansion.Value.Kind != ShellExpansionKind.Glob)
{
- pattern = ShellValueDomain.Unknown;
+ pattern = ShellValueDomainFacts.Unknown;
return false;
}
@@ -779,7 +779,7 @@ fragment.Expansion is null ||
ContainsParentTraversal(authored) ||
HasGlobBearingDotSegment(authored))
{
- pattern = ShellValueDomain.Unknown;
+ pattern = ShellValueDomainFacts.Unknown;
return false;
}
@@ -799,11 +799,11 @@ fragment.Expansion is null ||
ShellResolutionConsumer.BashArgument);
if (resolved.Resolved is null)
{
- pattern = ShellValueDomain.Unknown;
+ pattern = ShellValueDomainFacts.Unknown;
return false;
}
- pattern = new ShellValueDomain
+ pattern = new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Pattern,
Pattern = authored,
@@ -872,20 +872,20 @@ private static bool HasUnmodeledBraceExpansion(BashToken token) =>
token.Kind == BashTokenKind.Word &&
(token.Value.IndexOf('{') >= 0 || token.Value.IndexOf('}') >= 0);
- private static ShellValueDomain CreateFiniteDomain(IReadOnlyList values) =>
+ private static ShellValueDomainFacts CreateFiniteDomain(IReadOnlyList values) =>
values.Count switch
{
- 1 => new ShellValueDomain
+ 1 => new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { values[0] },
},
- >= 2 and <= 32 => new ShellValueDomain
+ >= 2 and <= 32 => new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.FiniteSet,
Values = Copy(values),
},
- _ => ShellValueDomain.Unknown,
+ _ => ShellValueDomainFacts.Unknown,
};
private static string[] Copy(IReadOnlyList values)
@@ -901,7 +901,7 @@ private static string[] Copy(IReadOnlyList values)
private sealed class BindingFrame
{
- internal BindingFrame(string name, ShellValueDomain domain)
+ internal BindingFrame(string name, ShellValueDomainFacts domain)
{
Name = name;
Domain = domain;
@@ -909,6 +909,6 @@ internal BindingFrame(string name, ShellValueDomain domain)
internal string Name { get; }
- internal ShellValueDomain Domain { get; }
+ internal ShellValueDomainFacts Domain { get; }
}
}
diff --git a/src/ShellSyntaxTree/Internal/Bash/Parsing/BashRedirectAnalysis.cs b/src/ShellSyntaxTree/Internal/Bash/Parsing/BashRedirectAnalysis.cs
index f5c6f69..413841d 100644
--- a/src/ShellSyntaxTree/Internal/Bash/Parsing/BashRedirectAnalysis.cs
+++ b/src/ShellSyntaxTree/Internal/Bash/Parsing/BashRedirectAnalysis.cs
@@ -18,23 +18,23 @@ namespace ShellSyntaxTree.Internal.Bash.Parsing;
///
internal static class BashRedirectAnalysis
{
- internal static IReadOnlyList Analyze(Clause clause)
+ internal static IReadOnlyList Analyze(Clause clause)
=> AnalyzeCore(clause, source: null, sourceTokens: null);
- internal static IReadOnlyList Analyze(
+ internal static IReadOnlyList Analyze(
Clause clause,
string source,
IReadOnlyList sourceTokens)
=> AnalyzeCore(clause, source, sourceTokens);
- private static IReadOnlyList AnalyzeCore(
+ private static IReadOnlyList AnalyzeCore(
Clause clause,
string? source,
IReadOnlyList? sourceTokens)
{
if (clause.Redirects.Count == 0)
{
- return Array.Empty();
+ return Array.Empty();
}
var elements = new List(clause.Redirects.Count);
@@ -46,7 +46,7 @@ private static IReadOnlyList AnalyzeCore(
}
}
- var result = new RedirectAnalysis[clause.Redirects.Count];
+ var result = new RedirectAnalysisFacts[clause.Redirects.Count];
for (var index = 0; index < result.Length; index++)
{
result[index] = index < elements.Count
@@ -62,7 +62,7 @@ private static IReadOnlyList AnalyzeCore(
return result;
}
- private static RedirectAnalysis Analyze(
+ private static RedirectAnalysisFacts Analyze(
int redirectIndex,
Redirect compatibility,
ClauseElement element,
@@ -114,45 +114,41 @@ RedirectOperation.FileOutput or
if (operation == RedirectOperation.Unknown)
{
- return new RedirectAnalysis
- {
- RedirectIndex = redirectIndex,
- Source = redirectSource,
- };
+ return Incomplete(redirectIndex);
}
var isComplete = !compatibility.IsDynamicSkip;
- return new RedirectAnalysis
+ return new RedirectAnalysisFacts
{
RedirectIndex = redirectIndex,
Source = redirectSource,
Operation = operation,
Target = isComplete
- ? new ShellValueDomain
+ ? new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { compatibility.Target },
}
- : ShellValueDomain.Unknown,
+ : ShellValueDomainFacts.Unknown,
IsPathRelevant = true,
IsComplete = isComplete,
};
}
- private static RedirectAnalysis AnalyzeHereString(
+ private static RedirectAnalysisFacts AnalyzeHereString(
int redirectIndex,
Redirect compatibility,
ClauseElement element,
- RedirectSource source)
+ RedirectSourceFacts source)
{
var target = compatibility.IsDynamicSkip
- ? ShellValueDomain.Unknown
- : new ShellValueDomain
+ ? ShellValueDomainFacts.Unknown
+ : new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { (element.Resolved ?? element.Value) + "\n" },
};
- return new RedirectAnalysis
+ return new RedirectAnalysisFacts
{
RedirectIndex = redirectIndex,
Source = source,
@@ -191,12 +187,12 @@ internal static ShellValue NormalizeHereStringOperand(ShellValue value)
return changed ? new ShellValue(value.Decoded, fragments) : value;
}
- private static RedirectAnalysis AnalyzeHereDocument(
+ private static RedirectAnalysisFacts AnalyzeHereDocument(
int redirectIndex,
string? source,
IReadOnlyList? sourceTokens,
ClauseElement element,
- RedirectSource redirectSource,
+ RedirectSourceFacts redirectSource,
bool stripLeadingTabs)
{
if (source is null || sourceTokens is null ||
@@ -246,7 +242,7 @@ token.HeredocBodyLength is null ||
StripLeadingTabs = stripLeadingTabs,
IsComplete = true,
};
- return new RedirectAnalysis
+ return new RedirectAnalysisFacts
{
RedirectIndex = redirectIndex,
Source = redirectSource,
@@ -259,9 +255,9 @@ token.HeredocBodyLength is null ||
return Incomplete(redirectIndex);
}
- private static RedirectAnalysis AnalyzeDescriptorTarget(
+ private static RedirectAnalysisFacts AnalyzeDescriptorTarget(
int redirectIndex,
- RedirectSource source,
+ RedirectSourceFacts source,
string authoredTarget,
string decodedTarget)
{
@@ -269,7 +265,7 @@ private static RedirectAnalysis AnalyzeDescriptorTarget(
{
if (string.Equals(authoredTarget, "&-", StringComparison.Ordinal))
{
- return new RedirectAnalysis
+ return new RedirectAnalysisFacts
{
RedirectIndex = redirectIndex,
Source = source,
@@ -292,7 +288,7 @@ private static RedirectAnalysis AnalyzeDescriptorTarget(
out var descriptor) &&
descriptor >= 0)
{
- return new RedirectAnalysis
+ return new RedirectAnalysisFacts
{
RedirectIndex = redirectIndex,
Source = source,
@@ -305,21 +301,18 @@ private static RedirectAnalysis AnalyzeDescriptorTarget(
}
}
- return new RedirectAnalysis
- {
- RedirectIndex = redirectIndex,
- Source = source,
- Operation = RedirectOperation.DescriptorDuplicate,
- };
+ // A computed target does not fit any closed descriptor alternative:
+ // duplicate and move records require a proved target descriptor.
+ return Incomplete(redirectIndex);
}
private static bool TryReadOperator(
string raw,
- out RedirectSource source,
+ out RedirectSourceFacts source,
out RedirectOperation operation,
out int length)
{
- source = new RedirectSource { Kind = RedirectSourceKind.Default };
+ source = new RedirectSourceFacts { Kind = RedirectSourceKind.Default };
operation = RedirectOperation.Unknown;
length = 0;
@@ -366,7 +359,7 @@ private static bool TryReadOperator(
out var descriptor) &&
descriptor >= 0)
{
- source = new RedirectSource
+ source = new RedirectSourceFacts
{
Kind = RedirectSourceKind.Descriptor,
Descriptor = descriptor,
@@ -374,7 +367,7 @@ private static bool TryReadOperator(
}
else
{
- source = new RedirectSource();
+ source = new RedirectSourceFacts();
if (raw.AsSpan(operatorStart).StartsWith("<<<", StringComparison.Ordinal))
{
operation = RedirectOperation.HereString;
@@ -437,7 +430,7 @@ private static bool TryReadOperator(
return true;
}
- source = new RedirectSource();
+ source = new RedirectSourceFacts();
return false;
}
@@ -460,7 +453,7 @@ private static bool TrySkipLineContinuation(
return true;
}
- private static RedirectAnalysis Incomplete(int redirectIndex) => new()
+ private static RedirectAnalysisFacts Incomplete(int redirectIndex) => new()
{
RedirectIndex = redirectIndex,
};
diff --git a/src/ShellSyntaxTree/Internal/Bash/Parsing/BashStructuralCoordinator.cs b/src/ShellSyntaxTree/Internal/Bash/Parsing/BashStructuralCoordinator.cs
index 646db28..a031c8f 100644
--- a/src/ShellSyntaxTree/Internal/Bash/Parsing/BashStructuralCoordinator.cs
+++ b/src/ShellSyntaxTree/Internal/Bash/Parsing/BashStructuralCoordinator.cs
@@ -67,6 +67,7 @@ private static BashParseResult ParseStructured(
CreateDependencySets(projection.Commands, analyzedFacts),
CreateValueProvenanceSets(projection.Commands, analyzedFacts),
CreateRedirectProvenanceSets(projection.Commands, analyzedFacts),
+ CreateRedirectAnalysisSets(projection.Commands, analyzedFacts),
analyzedForInPlans);
}
@@ -122,6 +123,21 @@ private static IReadOnlyList
return sets;
}
+ private static IReadOnlyList CreateRedirectAnalysisSets(
+ IReadOnlyList commands,
+ Func factsFactory)
+ {
+ var sets = new RedirectAnalysisSet[commands.Count];
+ for (var index = 0; index < sets.Length; index++)
+ {
+ var clause = commands[index].Clause;
+ var facts = factsFactory(new SimpleCommandSyntax { Clause = clause });
+ sets[index] = new RedirectAnalysisSet(clause, facts.Redirects);
+ }
+
+ return sets;
+ }
+
private static BashParseResult StructuralFailure(
string source,
string? reason,
@@ -138,6 +154,7 @@ private static BashParseResult StructuralFailure(
Array.Empty(),
Array.Empty(),
Array.Empty(),
+ Array.Empty(),
Array.Empty());
private sealed class StructuralCoordinator
@@ -1376,7 +1393,7 @@ private void RegisterFacts(
}
private static bool AreRedirectsComplete(
- IReadOnlyList redirects)
+ IReadOnlyList redirects)
{
foreach (var redirect in redirects)
{
@@ -1485,9 +1502,18 @@ private bool TryRegisterDecodedFacts(
return false;
}
+ if (!TryFindRedirectAnalysis(
+ innerResult.RedirectAnalysisSets,
+ source.Clause,
+ out var redirectAnalysis))
+ {
+ error = "decoded bash -c redirect analysis could not be mapped safely";
+ return false;
+ }
+
_facts.Add(clonedClause, new CommandOccurrenceFacts
{
- Redirects = ClearDecodedHereDocumentSpans(source.Redirects),
+ Redirects = ClearDecodedHereDocumentSpans(redirectAnalysis),
RedirectTargetProvenance = redirectProvenance,
CwdPathDependencies = cwdPathDependencies,
ValueProvenance = valueProvenance,
@@ -1510,10 +1536,10 @@ private bool TryRegisterDecodedFacts(
return true;
}
- private static IReadOnlyList ClearDecodedHereDocumentSpans(
- IReadOnlyList redirects)
+ private static IReadOnlyList ClearDecodedHereDocumentSpans(
+ IReadOnlyList redirects)
{
- var rewritten = new RedirectAnalysis[redirects.Count];
+ var rewritten = new RedirectAnalysisFacts[redirects.Count];
var changed = false;
for (var index = 0; index < rewritten.Length; index++)
{
@@ -1583,6 +1609,24 @@ private static bool TryFindRedirectProvenance(
return false;
}
+ private static bool TryFindRedirectAnalysis(
+ IReadOnlyList analysisSets,
+ Clause clause,
+ out IReadOnlyList redirects)
+ {
+ for (var index = 0; index < analysisSets.Count; index++)
+ {
+ if (ReferenceEquals(analysisSets[index].Clause, clause))
+ {
+ redirects = analysisSets[index].Redirects;
+ return true;
+ }
+ }
+
+ redirects = Array.Empty();
+ return false;
+ }
+
private static bool TryFindDependencies(
IReadOnlyList dependencySets,
Clause clause,
@@ -2063,11 +2107,32 @@ private static bool TryCloneDecodedNode(
Elements = ClauseElementProvenance.WithoutOuterSourceSpans(
simple.Clause.Elements),
};
+ var attachedExecutionRegions = new List(
+ executionRegions.Count);
+ foreach (var executionRegion in executionRegions)
+ {
+ var hostIndex = executionRegion.HostClauseElementIndex;
+ if (executionRegion.Origin == ExecutionRegionOrigin.CommandArgument &&
+ (hostIndex is null ||
+ hostIndex < 0 ||
+ hostIndex >= clonedClause.Elements.Count))
+ {
+ return false;
+ }
+
+ attachedExecutionRegions.Add(executionRegion with
+ {
+ HostArgument = hostIndex.HasValue
+ ? clonedClause.Elements[hostIndex.Value]
+ : null,
+ });
+ }
+
clone = new SimpleCommandSyntax
{
Clause = clonedClause,
Substitutions = substitutions,
- ExecutionRegions = executionRegions,
+ ExecutionRegions = attachedExecutionRegions,
};
referenceMap.Add(simple.Clause, clonedClause);
return true;
@@ -2264,6 +2329,7 @@ private static bool TryCloneDecodedNode(
clone = new ExecutionRegionSyntax
{
Origin = executionRegion.Origin,
+ HostArgument = executionRegion.HostArgument,
HostClauseElementIndex = executionRegion.HostClauseElementIndex,
Phase = executionRegion.Phase,
Timing = executionRegion.Timing,
diff --git a/src/ShellSyntaxTree/Internal/PublicCollection.cs b/src/ShellSyntaxTree/Internal/PublicCollection.cs
new file mode 100644
index 0000000..6b4f646
--- /dev/null
+++ b/src/ShellSyntaxTree/Internal/PublicCollection.cs
@@ -0,0 +1,46 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (C) 2026 - 2026 Aaron Stannard
+//
+// -----------------------------------------------------------------------
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace ShellSyntaxTree.Internal;
+
+internal static class PublicCollection
+{
+ internal static IReadOnlyList Copy(IReadOnlyList source)
+ {
+ if (source is null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (source.Count == 0)
+ {
+ return Array.Empty();
+ }
+
+ var copy = new T[source.Count];
+ for (var index = 0; index < source.Count; index++)
+ {
+ copy[index] = source[index];
+ }
+
+ return new ReadOnlyCollection(copy);
+ }
+
+ internal static IReadOnlyList Copy(IEnumerable source)
+ {
+ if (source is null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ return Copy(source is IReadOnlyList list
+ ? list
+ : new List(source));
+ }
+}
diff --git a/src/ShellSyntaxTree/Internal/Pwsh/Parsing/PwshForEachValueAnalysis.cs b/src/ShellSyntaxTree/Internal/Pwsh/Parsing/PwshForEachValueAnalysis.cs
index 449f693..4a28792 100644
--- a/src/ShellSyntaxTree/Internal/Pwsh/Parsing/PwshForEachValueAnalysis.cs
+++ b/src/ShellSyntaxTree/Internal/Pwsh/Parsing/PwshForEachValueAnalysis.cs
@@ -23,11 +23,11 @@ internal enum PwshIterationCardinality
internal sealed record PwshForEachAnalysisPlan(
string BindingName,
- IReadOnlyList OrderedCandidates,
+ IReadOnlyList OrderedCandidates,
int? AuthoredVisitCount,
PwshIterationCardinality Cardinality,
bool RequiresFixedPoint,
- ShellValueDomain Summary);
+ ShellValueDomainFacts Summary);
internal static class PwshForEachValueAnalysis
{
@@ -112,21 +112,21 @@ internal static PwshForEachAnalysisPlan CapturePlan(
{
return new PwshForEachAnalysisPlan(
bindingName,
- Array.Empty(),
+ Array.Empty(),
AuthoredVisitCount: 0,
PwshIterationCardinality.Never,
RequiresFixedPoint: false,
- ShellValueDomain.Unknown);
+ ShellValueDomainFacts.Unknown);
}
return isLiteralExpression
? new PwshForEachAnalysisPlan(
bindingName,
- new[] { ShellValueDomain.Unknown },
+ new[] { ShellValueDomainFacts.Unknown },
AuthoredVisitCount: 1,
PwshIterationCardinality.OneOrMore,
RequiresFixedPoint: false,
- ShellValueDomain.Unknown)
+ ShellValueDomainFacts.Unknown)
: UnknownPlan(bindingName, PwshIterationCardinality.ZeroOrMore);
}
@@ -135,25 +135,25 @@ private static PwshForEachAnalysisPlan UnknownPlan(
PwshIterationCardinality cardinality) =>
new(
bindingName,
- Array.Empty(),
+ Array.Empty(),
AuthoredVisitCount: null,
cardinality,
RequiresFixedPoint: true,
- ShellValueDomain.Unknown);
+ ShellValueDomainFacts.Unknown);
private static bool TryCaptureLiteralArray(
string raw,
- out IReadOnlyList orderedCandidates,
+ out IReadOnlyList orderedCandidates,
out int authoredVisitCount,
out PwshIterationCardinality cardinality,
out bool requiresFixedPoint,
- out ShellValueDomain summary)
+ out ShellValueDomainFacts summary)
{
- orderedCandidates = Array.Empty();
+ orderedCandidates = Array.Empty();
authoredVisitCount = 0;
cardinality = PwshIterationCardinality.Never;
requiresFixedPoint = false;
- summary = ShellValueDomain.Unknown;
+ summary = ShellValueDomainFacts.Unknown;
var index = 2;
var end = raw.Length - 1;
SkipWhitespace(raw, ref index, end);
@@ -163,7 +163,7 @@ private static bool TryCaptureLiteralArray(
}
var values = new List();
- var ordered = new List();
+ var ordered = new List();
var distinct = new HashSet(StringComparer.Ordinal);
var allStrings = true;
var summaryExceeded = false;
@@ -183,7 +183,7 @@ private static bool TryCaptureLiteralArray(
allStrings = false;
if (count <= ShellAnalysisLimits.MaxValueCandidates)
{
- ordered.Add(ShellValueDomain.Unknown);
+ ordered.Add(ShellValueDomainFacts.Unknown);
}
}
else
@@ -226,7 +226,7 @@ private static bool TryCaptureLiteralArray(
authoredVisitCount = count;
requiresFixedPoint = count > ShellAnalysisLimits.MaxValueCandidates;
orderedCandidates = requiresFixedPoint
- ? Array.Empty()
+ ? Array.Empty()
: ordered.ToArray();
if (allStrings && !summaryExceeded)
{
@@ -322,18 +322,18 @@ private static bool TryGetLiteralValue(ShellValue? value, out string literal)
return true;
}
- private static ShellValueDomain Exact(string value) => new()
+ private static ShellValueDomainFacts Exact(string value) => new()
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { value },
};
- private static ShellValueDomain CreateFiniteDomain(IReadOnlyList values) =>
+ private static ShellValueDomainFacts CreateFiniteDomain(IReadOnlyList values) =>
values.Count switch
{
- 0 => ShellValueDomain.Unknown,
+ 0 => ShellValueDomainFacts.Unknown,
1 => Exact(values[0]),
- _ => new ShellValueDomain
+ _ => new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.FiniteSet,
Values = Copy(values),
@@ -365,7 +365,7 @@ internal static class PwshPersistentStateMutation
internal static bool TryGetEffect(
Clause clause,
PwshDialect dialect,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
bool providerLocationUnknown,
out bool unknownCwd)
{
@@ -416,7 +416,7 @@ internal static bool TryGetEffect(
internal static bool MayEscapeChildScope(
Clause clause,
PwshDialect dialect,
- IReadOnlyList effectiveArguments)
+ IReadOnlyList effectiveArguments)
{
var verb = GetCanonicalVerb(clause, dialect);
if (verb is null)
@@ -503,7 +503,7 @@ internal static bool MayEscapeChildScope(
internal static bool MayEscapeChildRunspaceProcess(
Clause clause,
PwshDialect dialect,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
bool providerLocationUnknown)
{
var verb = GetCanonicalVerb(clause, dialect);
@@ -540,7 +540,7 @@ internal static bool MayEscapeChildRunspaceProcess(
internal static bool MayMutateAutomaticHome(
Clause clause,
PwshDialect dialect,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
bool providerLocationUnknown)
{
var verb = GetCanonicalVerb(clause, dialect);
@@ -587,7 +587,7 @@ private static bool IsVariableCommand(string verb) =>
private static bool VariableCommandMayTargetAutomaticHome(
string verb,
Clause clause,
- IReadOnlyList effectiveArguments)
+ IReadOnlyList effectiveArguments)
{
if (!TryGetAliasMutationNames(
verb,
@@ -654,7 +654,7 @@ internal static bool PreservesCommandResolution(
internal static bool TryGetCommandResolutionMutation(
Clause clause,
PwshDialect dialect,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
bool providerLocationUnknown,
out bool invalidatesAll,
out IReadOnlyList commandNames)
@@ -748,7 +748,7 @@ internal static bool TryGetCommandResolutionMutation(
private static bool TryGetAliasMutationNames(
string verb,
Clause clause,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
out IReadOnlyList commandNames)
{
var names = new List();
@@ -832,7 +832,7 @@ private static bool TryAddInlineAliasCommandNames(
ClauseElement element,
int elementIndex,
int separator,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
List names)
{
if (element.Kind is not (ArgKind.DynamicSkip or ArgKind.EnvVar))
@@ -869,7 +869,7 @@ private static bool TryAddInlineAliasCommandNames(
private static bool TryGetProviderMutationNames(
string verb,
Clause clause,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
out bool invalidatesAll,
out IReadOnlyList commandNames)
{
@@ -954,7 +954,7 @@ private static bool TryGetProviderMutationNames(
private static bool TryAddProviderCommandNames(
ClauseElement element,
int elementIndex,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
List names,
out bool selectedCommandProvider)
{
@@ -988,7 +988,7 @@ private static bool TryAddProviderCommandNames(
private static bool TryAddCommandNames(
ClauseElement element,
int elementIndex,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
List names)
{
if (!TryGetExactElementValues(
@@ -1014,7 +1014,7 @@ private static bool TryAddCommandNames(
private static bool TryGetExactElementValues(
ClauseElement element,
int elementIndex,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
out IReadOnlyList values)
{
foreach (var effective in effectiveArguments)
@@ -1254,7 +1254,7 @@ private static bool IsProvedChildLocalProviderMutation(
private static bool DynamicArgumentMayEscapeChildScope(
int elementIndex,
- IReadOnlyList effectiveArguments)
+ IReadOnlyList effectiveArguments)
{
foreach (var effective in effectiveArguments)
{
@@ -1477,13 +1477,13 @@ internal static bool IsProviderStateMutation(string verb, Clause clause) =>
IsProviderStateMutation(
verb,
clause,
- Array.Empty(),
+ Array.Empty(),
failOnUnproved: false);
private static bool IsProviderStateMutation(
string verb,
Clause clause,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
bool failOnUnproved,
bool providerLocationUnknown = false)
{
@@ -1513,7 +1513,7 @@ private static bool IsProviderStateMutation(
private static bool HasMutableOrUnprovedProviderTarget(
string verb,
Clause clause,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
bool failOnUnproved,
StateProviderSelection selection = StateProviderSelection.AnyMutable,
bool providerLocationUnknown = false)
@@ -1587,7 +1587,7 @@ private static bool CanSelectProvider(ProviderParameterRole parameter) =>
private static bool IsMutableOrUnprovedProviderTarget(
ClauseElement element,
int elementIndex,
- IReadOnlyList effectiveArguments,
+ IReadOnlyList effectiveArguments,
bool failOnUnproved,
StateProviderSelection selection,
bool providerLocationUnknown)
@@ -1645,7 +1645,7 @@ element.Resolved is not null &&
private static bool TargetDependsOnProviderLocation(
ClauseElement element,
int elementIndex,
- IReadOnlyList effectiveArguments)
+ IReadOnlyList effectiveArguments)
{
if (IsProviderLocationIndependent(element.Value) ||
IsProviderLocationIndependent(element.Raw) ||
@@ -3013,8 +3013,8 @@ private void RecordFacts(
SimpleCommandSyntax simple,
AnalysisContext input,
CommandOccurrenceFacts source,
- IReadOnlyList effective,
- IReadOnlyList redirects)
+ IReadOnlyList effective,
+ IReadOnlyList redirects)
{
var current = new CommandOccurrenceFacts
{
@@ -3051,7 +3051,7 @@ private void RecordFacts(
};
}
- private IReadOnlyList AnalyzeRedirects(
+ private IReadOnlyList AnalyzeRedirects(
CommandOccurrenceFacts source,
AnalysisContext input)
{
@@ -3061,7 +3061,7 @@ private IReadOnlyList AnalyzeRedirects(
return source.Redirects;
}
- var rewritten = new RedirectAnalysis[source.Redirects.Count];
+ var rewritten = new RedirectAnalysisFacts[source.Redirects.Count];
for (var index = 0; index < rewritten.Length; index++)
{
rewritten[index] = source.Redirects[index];
@@ -3082,7 +3082,7 @@ private IReadOnlyList AnalyzeRedirects(
rewritten[provenance.RedirectIndex] =
rewritten[provenance.RedirectIndex] with
{
- Target = ShellValueDomain.Unknown,
+ Target = ShellValueDomainFacts.Unknown,
};
if (!TryGetRedirectEvaluationContext(
provenance.InvocationScopeDepth,
@@ -3146,7 +3146,7 @@ rewritten[provenance.RedirectIndex] with
{
Target = resolvedAll
? CreateDomain(values)
- : ShellValueDomain.Unknown,
+ : ShellValueDomainFacts.Unknown,
};
}
@@ -3181,16 +3181,16 @@ private static bool IsRedirectHomePath(string value) =>
value[0] == '~' &&
(value.Length == 1 || value[1] is '/' or '\\');
- private static IReadOnlyList JoinRedirects(
- IReadOnlyList left,
- IReadOnlyList right)
+ private static IReadOnlyList JoinRedirects(
+ IReadOnlyList left,
+ IReadOnlyList right)
{
if (left.Count != right.Count)
{
- return Array.Empty();
+ return Array.Empty();
}
- var joined = new RedirectAnalysis[left.Count];
+ var joined = new RedirectAnalysisFacts[left.Count];
for (var index = 0; index < joined.Length; index++)
{
var leftFact = left[index];
@@ -3202,7 +3202,7 @@ private static IReadOnlyList JoinRedirects(
leftFact.IsPathRelevant != rightFact.IsPathRelevant ||
leftFact.HereDocument != rightFact.HereDocument)
{
- joined[index] = new RedirectAnalysis
+ joined[index] = new RedirectAnalysisFacts
{
RedirectIndex = leftFact.RedirectIndex,
};
@@ -3219,22 +3219,22 @@ private static IReadOnlyList JoinRedirects(
return joined;
}
- private static ShellValueDomain CreateDomain(IReadOnlyList values) =>
+ private static ShellValueDomainFacts CreateDomain(IReadOnlyList values) =>
values.Count switch
{
- 1 => new ShellValueDomain
+ 1 => new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { values[0] },
},
_ when values.Count > 1 &&
values.Count <= ShellAnalysisLimits.MaxValueCandidates =>
- new ShellValueDomain
+ new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.FiniteSet,
Values = values,
},
- _ => ShellValueDomain.Unknown,
+ _ => ShellValueDomainFacts.Unknown,
};
private PwshFlowResult AnalyzeList(CommandListSyntax list, AnalysisContext input)
@@ -3412,7 +3412,7 @@ private bool SimpleMayMutatePipelineState(SimpleCommandSyntax simple)
return PwshPersistentStateMutation.TryGetEffect(
simple.Clause,
_options.Dialect,
- Array.Empty(),
+ Array.Empty(),
providerLocationUnknown: false,
out _);
}
@@ -3869,14 +3869,14 @@ private bool TryGetElementDomain(
SimpleCommandSyntax simple,
int elementIndex,
AnalysisContext input,
- out ShellValueDomain domain)
+ out ShellValueDomainFacts domain)
{
if (TryGetElementValue(simple, elementIndex, 0, out var value))
{
return input.TryEvaluateValue(value, out domain);
}
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return false;
}
@@ -3920,7 +3920,7 @@ private CommandOccurrenceFacts GetFacts(SimpleCommandSyntax simple)
? new CommandOccurrenceFacts
{
EffectiveArguments = source.EffectiveArguments,
- WorkingDirectory = ShellValueDomain.Unknown,
+ WorkingDirectory = ShellValueDomainFacts.Unknown,
Redirects = RewriteRedirectsForUnknownState(source),
RedirectTargetProvenance = source.RedirectTargetProvenance,
CwdPathDependencies = source.CwdPathDependencies,
@@ -4048,8 +4048,8 @@ private SimpleCommandSyntax RewriteSimple(
};
}
- private static IReadOnlyList RewriteRedirectFacts(
- IReadOnlyList source,
+ private static IReadOnlyList RewriteRedirectFacts(
+ IReadOnlyList source,
IReadOnlyList provenance,
Clause clause)
{
@@ -4058,7 +4058,7 @@ private static IReadOnlyList RewriteRedirectFacts(
return source;
}
- var rewritten = new RedirectAnalysis[source.Count];
+ var rewritten = new RedirectAnalysisFacts[source.Count];
for (var index = 0; index < rewritten.Length; index++)
{
var fact = source[index];
@@ -4079,8 +4079,8 @@ private static IReadOnlyList RewriteRedirectFacts(
Target = hasProvenance
? fact.Target
: compatibility.IsDynamicSkip
- ? ShellValueDomain.Unknown
- : new ShellValueDomain
+ ? ShellValueDomainFacts.Unknown
+ : new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { compatibility.Target },
@@ -4654,15 +4654,15 @@ private void RecordUnvisitedBindingArguments(
{
case SimpleCommandSyntax simple:
var source = _factsFactory(simple);
- var effective = new List();
+ var effective = new List();
foreach (var provenance in source.ValueProvenance)
{
if (ReferencesBinding(provenance.Value, bindingName))
{
- effective.Add(new EffectiveArgument
+ effective.Add(new EffectiveArgumentFacts
{
ClauseElementIndex = provenance.ClauseElementIndex,
- Value = ShellValueDomain.Unknown,
+ Value = ShellValueDomainFacts.Unknown,
});
}
}
@@ -4670,7 +4670,7 @@ private void RecordUnvisitedBindingArguments(
_facts[simple.Clause] = new CommandOccurrenceFacts
{
EffectiveArguments = effective.ToArray(),
- WorkingDirectory = ShellValueDomain.Unknown,
+ WorkingDirectory = ShellValueDomainFacts.Unknown,
Redirects = RewriteRedirectsForUnknownState(source),
RedirectTargetProvenance = source.RedirectTargetProvenance,
CwdPathDependencies = source.CwdPathDependencies,
@@ -4721,7 +4721,7 @@ private void RecordUnvisitedBindingArguments(
}
}
- private IReadOnlyList RewriteRedirectsForUnknownState(
+ private IReadOnlyList RewriteRedirectsForUnknownState(
CommandOccurrenceFacts source)
{
if (source.Redirects.Count == 0)
@@ -4729,7 +4729,7 @@ private IReadOnlyList RewriteRedirectsForUnknownState(
return source.Redirects;
}
- var redirects = new RedirectAnalysis[source.Redirects.Count];
+ var redirects = new RedirectAnalysisFacts[source.Redirects.Count];
for (var index = 0; index < redirects.Length; index++)
{
var fact = source.Redirects[index];
@@ -4740,7 +4740,7 @@ private IReadOnlyList RewriteRedirectsForUnknownState(
out var value))
{
redirects[index] = fact.IsPathRelevant
- ? fact with { Target = ShellValueDomain.Unknown }
+ ? fact with { Target = ShellValueDomainFacts.Unknown }
: fact;
continue;
}
@@ -4754,12 +4754,12 @@ private IReadOnlyList RewriteRedirectsForUnknownState(
redirects[index] = fact with
{
Target = resolved.Resolved is not null && resolved.IsPath
- ? new ShellValueDomain
+ ? new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { resolved.Resolved },
}
- : ShellValueDomain.Unknown,
+ : ShellValueDomainFacts.Unknown,
};
}
@@ -4802,11 +4802,11 @@ private static bool ReferencesBinding(ShellValue value, string bindingName)
return false;
}
- private static IReadOnlyList JoinEffectiveArguments(
- IReadOnlyList left,
- IReadOnlyList right)
+ private static IReadOnlyList JoinEffectiveArguments(
+ IReadOnlyList left,
+ IReadOnlyList right)
{
- var joined = new Dictionary();
+ var joined = new Dictionary();
foreach (var argument in left)
{
joined[argument.ClauseElementIndex] = argument.Value;
@@ -4823,10 +4823,10 @@ private static IReadOnlyList JoinEffectiveArguments(
var indices = new List(joined.Keys);
indices.Sort();
- var result = new EffectiveArgument[indices.Count];
+ var result = new EffectiveArgumentFacts[indices.Count];
for (var index = 0; index < indices.Count; index++)
{
- result[index] = new EffectiveArgument
+ result[index] = new EffectiveArgumentFacts
{
ClauseElementIndex = indices[index],
Value = joined[indices[index]],
@@ -4836,25 +4836,25 @@ private static IReadOnlyList JoinEffectiveArguments(
return result;
}
- private static ShellValueDomain JoinWorkingDirectories(
- ShellValueDomain left,
- ShellValueDomain right) =>
+ private static ShellValueDomainFacts JoinWorkingDirectories(
+ ShellValueDomainFacts left,
+ ShellValueDomainFacts right) =>
left.Kind == ShellValueDomainKind.Exact &&
right.Kind == ShellValueDomainKind.Exact &&
left.Values.Count == 1 &&
right.Values.Count == 1 &&
string.Equals(left.Values[0], right.Values[0], StringComparison.Ordinal)
? left
- : ShellValueDomain.Unknown;
+ : ShellValueDomainFacts.Unknown;
- private static ShellValueDomain JoinDomains(
- ShellValueDomain left,
- ShellValueDomain right)
+ private static ShellValueDomainFacts JoinDomains(
+ ShellValueDomainFacts left,
+ ShellValueDomainFacts right)
{
if (left.Kind is not (ShellValueDomainKind.Exact or ShellValueDomainKind.FiniteSet) ||
right.Kind is not (ShellValueDomainKind.Exact or ShellValueDomainKind.FiniteSet))
{
- return ShellValueDomain.Unknown;
+ return ShellValueDomainFacts.Unknown;
}
var values = new List();
@@ -4873,7 +4873,7 @@ right.Kind is not (ShellValueDomainKind.Exact or ShellValueDomainKind.FiniteSet)
{
if (values.Count == ShellAnalysisLimits.MaxValueCandidates)
{
- return ShellValueDomain.Unknown;
+ return ShellValueDomainFacts.Unknown;
}
values.Add(value);
@@ -4882,29 +4882,29 @@ right.Kind is not (ShellValueDomainKind.Exact or ShellValueDomainKind.FiniteSet)
return values.Count switch
{
- 1 => new ShellValueDomain
+ 1 => new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = values.ToArray(),
},
- > 1 => new ShellValueDomain
+ > 1 => new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.FiniteSet,
Values = values.ToArray(),
},
- _ => ShellValueDomain.Unknown,
+ _ => ShellValueDomainFacts.Unknown,
};
}
private static string NormalizePath(string path) => path.Replace('\\', '/');
- private IReadOnlyList CreateEffectiveArguments(
+ private IReadOnlyList CreateEffectiveArguments(
IReadOnlyList provenance,
Clause clause,
AnalysisContext context,
bool includeUnresolved)
{
- var effective = new List();
+ var effective = new List();
foreach (var value in provenance)
{
if (value.ClauseElementIndex >= 0 &&
@@ -4917,7 +4917,7 @@ private IReadOnlyList CreateEffectiveArguments(
if (context.TryAnalyzeEffectiveValue(value.Value, out var domain))
{
- effective.Add(new EffectiveArgument
+ effective.Add(new EffectiveArgumentFacts
{
ClauseElementIndex = value.ClauseElementIndex,
Value = domain,
@@ -4935,7 +4935,7 @@ private IReadOnlyList CreateEffectiveArguments(
out domain) ||
IsPolicySensitiveValue(clause, value))
{
- effective.Add(new EffectiveArgument
+ effective.Add(new EffectiveArgumentFacts
{
ClauseElementIndex = value.ClauseElementIndex,
Value = domain,
@@ -4943,19 +4943,19 @@ private IReadOnlyList CreateEffectiveArguments(
}
else if (includeUnresolved && ContainsExpansion(value.Value))
{
- effective.Add(new EffectiveArgument
+ effective.Add(new EffectiveArgumentFacts
{
ClauseElementIndex = value.ClauseElementIndex,
- Value = ShellValueDomain.Unknown,
+ Value = ShellValueDomainFacts.Unknown,
});
}
}
else if (includeUnresolved && ContainsExpansion(value.Value))
{
- effective.Add(new EffectiveArgument
+ effective.Add(new EffectiveArgumentFacts
{
ClauseElementIndex = value.ClauseElementIndex,
- Value = ShellValueDomain.Unknown,
+ Value = ShellValueDomainFacts.Unknown,
});
}
}
@@ -5121,7 +5121,7 @@ private bool TryAnalyzeParserKnownValue(
ShellValue value,
bool? usesNativeBinding,
AnalysisContext context,
- out ShellValueDomain domain)
+ out ShellValueDomainFacts domain)
{
var homeDirectory = PwshResolver.GetHomeDirectory(_options);
var composed = new StringBuilder(value.Decoded.Length);
@@ -5139,7 +5139,7 @@ private bool TryAnalyzeParserKnownValue(
if (fragment.Kind != ShellValueFragmentKind.Expansion ||
fragment.Expansion is not ShellExpansionReference expansion)
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return false;
}
@@ -5160,7 +5160,7 @@ private bool TryAnalyzeParserKnownValue(
if (usesNativeBinding is null)
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return false;
}
@@ -5172,7 +5172,7 @@ private bool TryAnalyzeParserKnownValue(
if (!context.ConfiguredHomeAvailable || homeDirectory.Length == 0)
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return false;
}
@@ -5192,13 +5192,13 @@ private bool TryAnalyzeParserKnownValue(
{
if (usesNativeBinding is null)
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return false;
}
if (usesNativeBinding == true)
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return false;
}
}
@@ -5218,14 +5218,14 @@ private bool TryAnalyzeParserKnownValue(
(fragment.AllowedTransforms & ShellLexicalTransform.Variable) == 0 ||
homeDirectory.Length == 0)
{
- domain = ShellValueDomain.Unknown;
+ domain = ShellValueDomainFacts.Unknown;
return false;
}
composed.Append(homeDirectory);
}
- domain = new ShellValueDomain
+ domain = new ShellValueDomainFacts
{
Kind = ShellValueDomainKind.Exact,
Values = new[] { composed.ToString() },
@@ -5297,7 +5297,7 @@ private static bool ContainsReference(IReadOnlyList