git_policy_cli/command_options_query.rs
1//! What:
2//! Small read-only questions over a tokenized Git argument region.
3//! Why:
4//! Git applies options in order and the last use wins,
5//! so command modules ask for
6//! final states and positions instead of counting spellings.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! // lastOccurrence(parsed, id), isEnabled(parsed, id), positionalTokens(parsed, length), ...
11//! ```
12
13/// What:
14/// Bring the tokenizer result types into this file.
15/// Why:
16/// Every question reads the lists the tokenizer produced.
17///
18/// In TS you'd write (pseudocode):
19/// ```ts
20/// import { Boundary, Occurrence, OptionValue, ParsedOptions } from './command_options.ts';
21/// ```
22use super::command_options::{Boundary, Occurrence, OptionValue, ParsedOptions, WrapperOccurrence};
23/// `OsString` is owned operating-system text of raw bytes (sibling `String` must be UTF-8).
24use std::ffi::OsString;
25
26/// What:
27/// The final use of the option `id`,
28/// or nothing.
29/// `&ParsedOptions` borrows the result
30/// read-only;
31/// `u16` is the identifier type of table rows.
32/// Why:
33/// Git's variables keep what the last matching option wrote.
34///
35/// In TS you'd write (pseudocode):
36/// ```ts
37/// const last = parsed.occurrences.findLast(o => o.id === id);
38/// ```
39pub fn last_occurrence(parsed: &ParsedOptions, id: u16) -> Option<Occurrence> {
40 // `mut` lets later matches replace earlier ones during the single pass.
41 let mut last: Option<Occurrence> = None;
42 // `for occurrence in &parsed.occurrences` borrows each item in order.
43 for occurrence in &parsed.occurrences {
44 if occurrence.id == id {
45 // `*occurrence` copies the small record out of the borrowed list.
46 last = Some(*occurrence);
47 }
48 }
49 return last;
50}
51
52/// What:
53/// Whether the option was written at all,
54/// in either form.
55/// Why:
56/// An explicit `--no-only` is a stated choice even though it turns the option off.
57///
58/// In TS you'd write (pseudocode):
59/// ```ts
60/// const stated = parsed.occurrences.some(o => o.id === id);
61/// ```
62pub fn is_stated(parsed: &ParsedOptions, id: u16) -> bool {
63 return last_occurrence(parsed, id).is_some();
64}
65
66/// What:
67/// Whether the option is on after every use was applied.
68/// Why:
69/// `--all --no-all` leaves `all` off,
70/// exactly as Git's `OPT_BOOL` does.
71///
72/// In TS you'd write (pseudocode):
73/// ```ts
74/// const enabled = last !== undefined && !last.negated;
75/// ```
76pub fn is_enabled(parsed: &ParsedOptions, id: u16) -> bool {
77 // `if let Some(last) = ...` runs only when the option was written.
78 if let Some(last) = last_occurrence(parsed, id) {
79 return !last.negated;
80 }
81 return false;
82}
83
84/// What:
85/// Every non-option token index in order:
86/// those before the boundary,
87/// then every
88/// token after it.
89/// `length` is the region's token count.
90/// Why:
91/// Git hands the command both groups as its remaining arguments.
92/// Gotcha:
93/// Under `Boundary::NonOption` the boundary token itself is positional;
94/// under
95/// `--` and `--end-of-options` it is not.
96///
97/// In TS you'd write (pseudocode):
98/// ```ts
99/// const positionals = [...parsed.leading, ...indexesAfterBoundary];
100/// ```
101pub fn positional_tokens(parsed: &ParsedOptions, length: usize) -> Vec<usize> {
102 // `.clone()` copies the index list so the result is independently owned.
103 let mut tokens: Vec<usize> = parsed.leading.clone();
104 // What: `match` picks one arm per variant and unpacks the index a variant holds; the
105 // compiler refuses the code if a variant is forgotten.
106 // Why: `next` is the first index from which every token is positional.
107 //
108 // In TS you'd write (pseudocode):
109 // ```ts
110 // switch (boundary.kind) { case 'end': next = length; break; /* ... */ }
111 // ```
112 let mut next: usize = match parsed.boundary {
113 Boundary::End => length,
114 Boundary::DashDash(at) => at + 1,
115 Boundary::EndOfOptions(at) => at + 1,
116 Boundary::NonOption(at) => at,
117 };
118 while next < length {
119 tokens.push(next);
120 next += 1;
121 }
122 return tokens;
123}
124
125/// What:
126/// The exact bytes of an option value.
127/// `&[u8]` borrows them from `arguments`.
128/// Why:
129/// Values are compared as bytes;
130/// converting to `String` would reject non-UTF-8 text.
131///
132/// In TS you'd write (pseudocode):
133/// ```ts
134/// const text = 'offset' in value ? args[value.token].slice(value.offset) : args[value.token];
135/// ```
136pub fn value_bytes(arguments: &[OsString], value: OptionValue) -> &[u8] {
137 // `(usize, usize)` is a pair; a detached value is the whole token, so it starts at byte 0.
138 let (token, offset): (usize, usize) = match value {
139 OptionValue::Attached { token, offset } => (token, offset),
140 OptionValue::Detached { token } => (token, 0),
141 };
142 // `&bytes[offset..]` borrows from `offset` to the end.
143 return &arguments[token].as_encoded_bytes()[offset..];
144}
145
146/// What:
147/// Wrapper-only flags found in option position,
148/// split into the command's own escape
149/// hatch and every other flag the caller listed.
150/// Why:
151/// A rule asks "was my hatch written?",
152/// while the caller removes all of them by position.
153///
154/// In TS you'd write (pseudocode):
155/// ```ts
156/// type WrapperFlags = { escape: number[]; other: WrapperOccurrence[] };
157/// ```
158#[derive(Clone, Debug, Eq, PartialEq)]
159pub struct WrapperFlags {
160 /// Region token indexes of the command's own escape hatch.
161 pub escape: Vec<usize>,
162 /// Other wrapper flags;
163 /// `flag` indexes the caller's list.
164 pub other: Vec<WrapperOccurrence>,
165}
166
167/// What:
168/// Split the wrapper occurrences of a parse whose flag list was built as
169/// "own escape hatch first,
170/// then the caller's flags".
171/// `&[WrapperOccurrence]` borrows
172/// the occurrence list of that parse.
173/// Why:
174/// Every command module builds its list that way,
175/// so the caller's indexes are the
176/// tokenizer's indexes minus one.
177///
178/// In TS you'd write (pseudocode):
179/// ```ts
180/// const escape = wrapper.filter(w => w.flag === 0).map(w => w.token);
181/// const other = wrapper.filter(w => w.flag > 0).map(w => ({ flag: w.flag - 1, token: w.token }));
182/// ```
183pub fn split_wrapper_flags(occurrences: &[WrapperOccurrence]) -> WrapperFlags {
184 let mut escape: Vec<usize> = Vec::<usize>::new();
185 let mut other: Vec<WrapperOccurrence> = Vec::<WrapperOccurrence>::new();
186 for occurrence in occurrences {
187 if occurrence.flag == 0 {
188 escape.push(occurrence.token);
189 } else {
190 other.push(WrapperOccurrence {
191 flag: occurrence.flag - 1,
192 token: occurrence.token,
193 });
194 }
195 }
196 return WrapperFlags { escape, other };
197}
198
199/// What:
200/// Copy `arguments` without the tokens at `removed`,
201/// which are indexes relative to
202/// `offset` (the index of the first region token).
203/// Why:
204/// Wrapper-only flags are deleted by position,
205/// so an equal-looking value or path stays.
206///
207/// In TS you'd write (pseudocode):
208/// ```ts
209/// const kept = args.filter((_, i) => !removed.includes(i - offset));
210/// ```
211pub fn without_tokens(arguments: &[OsString], offset: usize, removed: &[usize]) -> Vec<OsString> {
212 let mut kept: Vec<OsString> = Vec::<OsString>::with_capacity(arguments.len());
213 let mut index: usize = 0;
214 while index < arguments.len() {
215 // `index >= offset` first, so the subtraction cannot go below zero.
216 let dropped: bool = index >= offset && removed.contains(&(index - offset));
217 if !dropped {
218 // `.clone()` copies the argument bytes into the owned result.
219 kept.push(arguments[index].clone());
220 }
221 index += 1;
222 }
223 return kept;
224}