Skip to main content

shuck_linter/facts/
loop_headers.rs

1use super::*;
2
3#[derive(Debug, Clone, Copy)]
4pub struct LoopHeaderWordFact<'a> {
5    word: &'a Word,
6    classification: WordClassification,
7    has_all_elements_array_expansion: bool,
8    has_unquoted_command_substitution: bool,
9    contains_line_oriented_substitution: bool,
10    contains_ls_substitution: bool,
11    contains_find_substitution: bool,
12}
13
14impl<'a> LoopHeaderWordFact<'a> {
15    pub fn word(&self) -> &'a Word {
16        self.word
17    }
18
19    pub fn span(&self) -> Span {
20        self.word.span
21    }
22
23    pub fn classification(&self) -> WordClassification {
24        self.classification
25    }
26
27    pub fn has_command_substitution(&self) -> bool {
28        self.classification.has_command_substitution()
29    }
30
31    pub fn has_all_elements_array_expansion(&self) -> bool {
32        self.has_all_elements_array_expansion
33    }
34
35    pub fn has_unquoted_command_substitution(&self) -> bool {
36        self.has_unquoted_command_substitution
37    }
38
39    pub fn contains_line_oriented_substitution(&self) -> bool {
40        self.contains_line_oriented_substitution
41    }
42
43    pub fn contains_ls_substitution(&self) -> bool {
44        self.contains_ls_substitution
45    }
46
47    pub fn contains_find_substitution(&self) -> bool {
48        self.contains_find_substitution
49    }
50}
51
52#[derive(Debug, Clone)]
53pub struct ForHeaderFact<'a> {
54    command: &'a ForCommand,
55    command_id: CommandId,
56    nested_word_command: bool,
57    words: Box<[LoopHeaderWordFact<'a>]>,
58}
59
60impl<'a> ForHeaderFact<'a> {
61    pub fn command(&self) -> &'a ForCommand {
62        self.command
63    }
64
65    pub fn command_id(&self) -> CommandId {
66        self.command_id
67    }
68
69    pub fn span(&self) -> Span {
70        self.command.span
71    }
72
73    pub fn is_nested_word_command(&self) -> bool {
74        self.nested_word_command
75    }
76
77    pub fn words(&self) -> &[LoopHeaderWordFact<'a>] {
78        &self.words
79    }
80
81    pub fn has_command_substitution(&self) -> bool {
82        self.words
83            .iter()
84            .any(LoopHeaderWordFact::has_command_substitution)
85    }
86
87    pub fn has_find_substitution(&self) -> bool {
88        self.words
89            .iter()
90            .any(LoopHeaderWordFact::contains_find_substitution)
91    }
92}
93
94#[derive(Debug, Clone)]
95pub struct SelectHeaderFact<'a> {
96    command: &'a SelectCommand,
97    command_id: CommandId,
98    nested_word_command: bool,
99    words: Box<[LoopHeaderWordFact<'a>]>,
100}
101
102impl<'a> SelectHeaderFact<'a> {
103    pub fn command(&self) -> &'a SelectCommand {
104        self.command
105    }
106
107    pub fn command_id(&self) -> CommandId {
108        self.command_id
109    }
110
111    pub fn span(&self) -> Span {
112        self.command.span
113    }
114
115    pub fn is_nested_word_command(&self) -> bool {
116        self.nested_word_command
117    }
118
119    pub fn words(&self) -> &[LoopHeaderWordFact<'a>] {
120        &self.words
121    }
122
123    pub fn has_command_substitution(&self) -> bool {
124        self.words
125            .iter()
126            .any(LoopHeaderWordFact::has_command_substitution)
127    }
128
129    pub fn has_find_substitution(&self) -> bool {
130        self.words
131            .iter()
132            .any(LoopHeaderWordFact::contains_find_substitution)
133    }
134}
135
136pub(crate) fn build_for_header_facts<'a>(
137    commands: &[CommandFact<'a>],
138    command_fact_indices_by_id: &[Option<usize>],
139    command_ids_by_span: &CommandLookupIndex,
140    locator: Locator<'_>,
141) -> Vec<ForHeaderFact<'a>> {
142    commands
143        .iter()
144        .filter_map(|fact| {
145            let Command::Compound(CompoundCommand::For(command)) = fact.command() else {
146                return None;
147            };
148
149            Some(ForHeaderFact {
150                command,
151                command_id: fact.id(),
152                nested_word_command: fact.is_nested_word_command(),
153                words: build_loop_header_word_facts(
154                    command.words.iter().flat_map(|words| words.iter()),
155                    commands,
156                    command_fact_indices_by_id,
157                    command_ids_by_span,
158                    locator,
159                    fact.shell_behavior().shell_dialect(),
160                ),
161            })
162        })
163        .collect()
164}
165
166pub(crate) fn build_select_header_facts<'a>(
167    commands: &[CommandFact<'a>],
168    command_fact_indices_by_id: &[Option<usize>],
169    command_ids_by_span: &CommandLookupIndex,
170    locator: Locator<'_>,
171) -> Vec<SelectHeaderFact<'a>> {
172    commands
173        .iter()
174        .filter_map(|fact| {
175            let Command::Compound(CompoundCommand::Select(command)) = fact.command() else {
176                return None;
177            };
178
179            Some(SelectHeaderFact {
180                command,
181                command_id: fact.id(),
182                nested_word_command: fact.is_nested_word_command(),
183                words: build_loop_header_word_facts(
184                    command.words.iter(),
185                    commands,
186                    command_fact_indices_by_id,
187                    command_ids_by_span,
188                    locator,
189                    fact.shell_behavior().shell_dialect(),
190                ),
191            })
192        })
193        .collect()
194}
195
196pub(crate) fn build_loop_header_word_facts<'a>(
197    words: impl IntoIterator<Item = &'a Word>,
198    commands: &[CommandFact<'a>],
199    command_fact_indices_by_id: &[Option<usize>],
200    command_ids_by_span: &CommandLookupIndex,
201    locator: Locator<'_>,
202    shell_dialect: shuck_semantic::ShellDialect,
203) -> Box<[LoopHeaderWordFact<'a>]> {
204    let source = locator.source();
205    words
206        .into_iter()
207        .map(|word| {
208            let classification = classify_word(word, source);
209            LoopHeaderWordFact {
210                word,
211                classification,
212                has_all_elements_array_expansion:
213                    word_spans::word_has_all_elements_array_expansion_syntax(
214                        word,
215                        source,
216                        shell_dialect,
217                    ) || !word_spans::all_elements_array_expansion_part_spans(
218                        word,
219                        locator,
220                        shell_dialect,
221                    )
222                    .is_empty(),
223                has_unquoted_command_substitution: classification.has_command_substitution()
224                    && !word_spans::unquoted_command_substitution_part_spans(word).is_empty(),
225                contains_line_oriented_substitution: word_contains_line_oriented_substitution(
226                    word,
227                    commands,
228                    command_fact_indices_by_id,
229                    command_ids_by_span,
230                ),
231                contains_ls_substitution: word_contains_command_substitution_named(
232                    word,
233                    "ls",
234                    commands,
235                    command_fact_indices_by_id,
236                    command_ids_by_span,
237                ),
238                contains_find_substitution: word_contains_find_substitution(
239                    word,
240                    commands,
241                    command_fact_indices_by_id,
242                    command_ids_by_span,
243                ),
244            }
245        })
246        .collect::<Vec<_>>()
247        .into_boxed_slice()
248}