shuck_linter/facts/
loop_headers.rs1use 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}