Skip to main content

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}