darklua_core/rules/
append_text_comment.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::sync::OnceLock;
4
5use crate::nodes::{
6    Block, BlockTokens, DoTokens, FunctionBodyTokens, GenericForTokens, Identifier,
7    IfStatementTokens, LastStatement, LocalAssignTokens, LocalFunctionTokens, NumericForTokens,
8    ParentheseExpression, ParentheseTokens, Prefix, RepeatTokens, ReturnTokens, Statement, Token,
9    TriviaKind, TypeDeclarationTokens, Variable, WhileTokens,
10};
11use crate::rules::{
12    verify_property_collisions, verify_required_any_properties, Context, Rule, RuleConfiguration,
13    RuleConfigurationError, RuleProcessResult, RuleProperties,
14};
15
16use super::{FlawlessRule, ShiftTokenLine};
17
18pub const APPEND_TEXT_COMMENT_RULE_NAME: &str = "append_text_comment";
19
20/// A rule to append a comment at the beginning or the end of each file.
21#[derive(Debug, Default)]
22pub struct AppendTextComment {
23    text_value: OnceLock<Result<String, String>>,
24    text_content: TextContent,
25    location: AppendLocation,
26}
27
28impl AppendTextComment {
29    pub fn new(value: impl Into<String>) -> Self {
30        Self {
31            text_value: Default::default(),
32            text_content: TextContent::Value(value.into()),
33            location: Default::default(),
34        }
35    }
36
37    pub fn from_file_content(file_path: impl Into<PathBuf>) -> Self {
38        Self {
39            text_value: Default::default(),
40            text_content: TextContent::FilePath(file_path.into()),
41            location: Default::default(),
42        }
43    }
44
45    pub fn at_end(mut self) -> Self {
46        self.location = AppendLocation::End;
47        self
48    }
49
50    fn text(&self, project_path: &Path) -> Result<String, String> {
51        self.text_value
52            .get_or_init(|| {
53                match &self.text_content {
54                    TextContent::None => Err("".to_owned()),
55                    TextContent::Value(value) => Ok(value.clone()),
56                    TextContent::FilePath(file_path) => {
57                        fs::read_to_string(project_path.join(file_path)).map_err(|err| {
58                            format!("unable to read file `{}`: {}", file_path.display(), err)
59                        })
60                    }
61                }
62                .map(|content| {
63                    if content.is_empty() {
64                        "".to_owned()
65                    } else if content.contains('\n') {
66                        let mut equal_count = 0;
67
68                        let close_comment = loop {
69                            let close_comment = format!("]{}]", "=".repeat(equal_count));
70                            if !content.contains(&close_comment) {
71                                break close_comment;
72                            }
73                            equal_count += 1;
74                        };
75
76                        format!(
77                            "--[{}[\n{}\n{}",
78                            "=".repeat(equal_count),
79                            content,
80                            close_comment
81                        )
82                    } else {
83                        format!("--{}", content)
84                    }
85                })
86            })
87            .clone()
88    }
89}
90
91impl Rule for AppendTextComment {
92    fn process(&self, block: &mut Block, context: &Context) -> RuleProcessResult {
93        let text = self.text(context.project_location())?;
94
95        if text.is_empty() {
96            return Ok(());
97        }
98
99        let shift_lines = text.lines().count();
100        ShiftTokenLine::new(shift_lines as isize).flawless_process(block, context);
101
102        match self.location {
103            AppendLocation::Start => {
104                if let Some(statement) = block.first_mut_statement() {
105                    match statement {
106                        Statement::Assign(assign_statement) => {
107                            let variable = assign_statement
108                                .iter_mut_variables()
109                                .next()
110                                .ok_or("an assign statement must have at least one variable")?;
111                            self.location
112                                .append_comment(variable_get_first_token(variable), text);
113                        }
114                        Statement::Do(do_statement) => {
115                            if let Some(tokens) = do_statement.mutate_tokens() {
116                                self.location.append_comment(&mut tokens.r#do, text);
117                            } else {
118                                let mut token = Token::from_content("do");
119                                self.location.append_comment(&mut token, text);
120
121                                do_statement.set_tokens(DoTokens {
122                                    r#do: token,
123                                    end: Token::from_content("end"),
124                                });
125                            }
126                        }
127                        Statement::Call(call) => {
128                            self.location
129                                .append_comment(prefix_get_first_token(call.mutate_prefix()), text);
130                        }
131                        Statement::CompoundAssign(compound_assign) => {
132                            self.location.append_comment(
133                                variable_get_first_token(compound_assign.mutate_variable()),
134                                text,
135                            );
136                        }
137                        Statement::Function(function) => {
138                            if let Some(tokens) = function.mutate_tokens() {
139                                self.location.append_comment(&mut tokens.function, text);
140                            } else {
141                                let mut token = Token::from_content("function");
142                                self.location.append_comment(&mut token, text);
143
144                                function.set_tokens(FunctionBodyTokens {
145                                    function: token,
146                                    opening_parenthese: Token::from_content("("),
147                                    closing_parenthese: Token::from_content(")"),
148                                    end: Token::from_content("end"),
149                                    parameter_commas: Vec::new(),
150                                    variable_arguments: None,
151                                    variable_arguments_colon: None,
152                                    return_type_colon: None,
153                                });
154                            }
155                        }
156                        Statement::GenericFor(generic_for) => {
157                            if let Some(tokens) = generic_for.mutate_tokens() {
158                                self.location.append_comment(&mut tokens.r#for, text);
159                            } else {
160                                let mut token = Token::from_content("for");
161                                self.location.append_comment(&mut token, text);
162
163                                generic_for.set_tokens(GenericForTokens {
164                                    r#for: token,
165                                    r#in: Token::from_content("in"),
166                                    r#do: Token::from_content("do"),
167                                    end: Token::from_content("end"),
168                                    identifier_commas: Vec::new(),
169                                    value_commas: Vec::new(),
170                                });
171                            }
172                        }
173                        Statement::If(if_statement) => {
174                            if let Some(tokens) = if_statement.mutate_tokens() {
175                                self.location.append_comment(&mut tokens.r#if, text);
176                            } else {
177                                let mut token = Token::from_content("if");
178                                self.location.append_comment(&mut token, text);
179
180                                if_statement.set_tokens(IfStatementTokens {
181                                    r#if: token,
182                                    then: Token::from_content("then"),
183                                    end: Token::from_content("end"),
184                                    r#else: None,
185                                });
186                            }
187                        }
188                        Statement::LocalAssign(local_assign) => {
189                            if let Some(tokens) = local_assign.mutate_tokens() {
190                                self.location.append_comment(&mut tokens.local, text);
191                            } else {
192                                let mut token = Token::from_content("local");
193                                self.location.append_comment(&mut token, text);
194
195                                local_assign.set_tokens(LocalAssignTokens {
196                                    local: token,
197                                    equal: None,
198                                    variable_commas: Vec::new(),
199                                    value_commas: Vec::new(),
200                                });
201                            }
202                        }
203                        Statement::LocalFunction(local_function) => {
204                            if let Some(tokens) = local_function.mutate_tokens() {
205                                self.location.append_comment(&mut tokens.local, text);
206                            } else {
207                                let mut token = Token::from_content("local");
208                                self.location.append_comment(&mut token, text);
209
210                                local_function.set_tokens(LocalFunctionTokens {
211                                    local: token,
212                                    function_body: FunctionBodyTokens {
213                                        function: Token::from_content("function"),
214                                        opening_parenthese: Token::from_content("("),
215                                        closing_parenthese: Token::from_content(")"),
216                                        end: Token::from_content("end"),
217                                        parameter_commas: Vec::new(),
218                                        variable_arguments: None,
219                                        variable_arguments_colon: None,
220                                        return_type_colon: None,
221                                    },
222                                });
223                            }
224                        }
225                        Statement::NumericFor(numeric_for) => {
226                            if let Some(tokens) = numeric_for.mutate_tokens() {
227                                self.location.append_comment(&mut tokens.r#for, text);
228                            } else {
229                                let mut token = Token::from_content("for");
230                                self.location.append_comment(&mut token, text);
231
232                                numeric_for.set_tokens(NumericForTokens {
233                                    r#for: token,
234                                    equal: Token::from_content("="),
235                                    r#do: Token::from_content("do"),
236                                    end: Token::from_content("end"),
237                                    end_comma: Token::from_content(","),
238                                    step_comma: None,
239                                });
240                            }
241                        }
242                        Statement::Repeat(repeat) => {
243                            if let Some(tokens) = repeat.mutate_tokens() {
244                                self.location.append_comment(&mut tokens.repeat, text);
245                            } else {
246                                let mut token = Token::from_content("repeat");
247                                self.location.append_comment(&mut token, text);
248
249                                repeat.set_tokens(RepeatTokens {
250                                    repeat: token,
251                                    until: Token::from_content("until"),
252                                });
253                            }
254                        }
255                        Statement::While(while_statement) => {
256                            if let Some(tokens) = while_statement.mutate_tokens() {
257                                self.location.append_comment(&mut tokens.r#while, text);
258                            } else {
259                                let mut token = Token::from_content("while");
260                                self.location.append_comment(&mut token, text);
261
262                                while_statement.set_tokens(WhileTokens {
263                                    r#while: token,
264                                    r#do: Token::from_content("do"),
265                                    end: Token::from_content("end"),
266                                });
267                            }
268                        }
269                        Statement::TypeDeclaration(type_declaration) => {
270                            let is_exported = type_declaration.is_exported();
271                            if let Some(tokens) = type_declaration.mutate_tokens() {
272                                if is_exported {
273                                    self.location.append_comment(
274                                        tokens
275                                            .export
276                                            .get_or_insert_with(|| Token::from_content("export")),
277                                        text,
278                                    );
279                                } else {
280                                    self.location.append_comment(&mut tokens.r#type, text);
281                                }
282                            } else if is_exported {
283                                let mut token = Token::from_content("export");
284                                self.location.append_comment(&mut token, text);
285
286                                type_declaration.set_tokens(TypeDeclarationTokens {
287                                    r#type: Token::from_content("type"),
288                                    equal: Token::from_content("="),
289                                    export: Some(token),
290                                });
291                            } else {
292                                let mut token = Token::from_content("type");
293                                self.location.append_comment(&mut token, text);
294
295                                type_declaration.set_tokens(TypeDeclarationTokens {
296                                    r#type: token,
297                                    equal: Token::from_content("="),
298                                    export: None,
299                                });
300                            }
301                        }
302                    }
303                } else if let Some(statement) = block.mutate_last_statement() {
304                    match statement {
305                        LastStatement::Break(token) => {
306                            self.location.append_comment(
307                                token.get_or_insert_with(|| Token::from_content("break")),
308                                text,
309                            );
310                        }
311                        LastStatement::Continue(token) => {
312                            self.location.append_comment(
313                                token.get_or_insert_with(|| Token::from_content("continue")),
314                                text,
315                            );
316                        }
317                        LastStatement::Return(return_statement) => {
318                            if let Some(tokens) = return_statement.mutate_tokens() {
319                                self.location.append_comment(&mut tokens.r#return, text);
320                            } else {
321                                let mut token = Token::from_content("return");
322                                self.location.append_comment(&mut token, text);
323
324                                return_statement.set_tokens(ReturnTokens {
325                                    r#return: token,
326                                    commas: Vec::new(),
327                                });
328                            }
329                        }
330                    }
331                } else {
332                    self.location.write_to_block(block, text);
333                }
334            }
335            AppendLocation::End => {
336                self.location.write_to_block(block, text);
337            }
338        }
339
340        Ok(())
341    }
342}
343
344fn variable_get_first_token(variable: &mut Variable) -> &mut Token {
345    match variable {
346        Variable::Identifier(identifier) => identifier_get_first_token(identifier),
347        Variable::Field(field_expression) => {
348            prefix_get_first_token(field_expression.mutate_prefix())
349        }
350        Variable::Index(index_expression) => {
351            prefix_get_first_token(index_expression.mutate_prefix())
352        }
353    }
354}
355
356fn prefix_get_first_token(prefix: &mut Prefix) -> &mut Token {
357    let mut current = prefix;
358    loop {
359        match current {
360            Prefix::Call(call) => {
361                current = call.mutate_prefix();
362            }
363            Prefix::Field(field_expression) => {
364                current = field_expression.mutate_prefix();
365            }
366            Prefix::Index(index_expression) => {
367                current = index_expression.mutate_prefix();
368            }
369            Prefix::Identifier(identifier) => break identifier_get_first_token(identifier),
370            Prefix::Parenthese(parenthese_expression) => {
371                break parentheses_get_first_token(parenthese_expression)
372            }
373        }
374    }
375}
376
377fn identifier_get_first_token(identifier: &mut Identifier) -> &mut Token {
378    if identifier.get_token().is_none() {
379        let name = identifier.get_name().to_owned();
380        identifier.set_token(Token::from_content(name));
381    }
382    identifier.mutate_token().unwrap()
383}
384
385fn parentheses_get_first_token(parentheses: &mut ParentheseExpression) -> &mut Token {
386    if parentheses.get_tokens().is_none() {
387        parentheses.set_tokens(ParentheseTokens {
388            left_parenthese: Token::from_content("("),
389            right_parenthese: Token::from_content(")"),
390        });
391    }
392    &mut parentheses.mutate_tokens().unwrap().left_parenthese
393}
394
395impl RuleConfiguration for AppendTextComment {
396    fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
397        verify_required_any_properties(&properties, &["text", "file"])?;
398        verify_property_collisions(&properties, &["text", "file"])?;
399
400        for (key, value) in properties {
401            match key.as_str() {
402                "text" => {
403                    self.text_content = TextContent::Value(value.expect_string(&key)?);
404                }
405                "file" => {
406                    self.text_content =
407                        TextContent::FilePath(PathBuf::from(value.expect_string(&key)?));
408                }
409                "location" => {
410                    self.location = match value.expect_string(&key)?.as_str() {
411                        "start" => AppendLocation::Start,
412                        "end" => AppendLocation::End,
413                        unexpected => {
414                            return Err(RuleConfigurationError::UnexpectedValue {
415                                property: "location".to_owned(),
416                                message: format!(
417                                    "invalid value `{}` (must be `start` or `end`)",
418                                    unexpected
419                                ),
420                            })
421                        }
422                    };
423                }
424                _ => return Err(RuleConfigurationError::UnexpectedProperty(key)),
425            }
426        }
427
428        Ok(())
429    }
430
431    fn get_name(&self) -> &'static str {
432        APPEND_TEXT_COMMENT_RULE_NAME
433    }
434
435    fn serialize_to_properties(&self) -> RuleProperties {
436        let mut properties = RuleProperties::new();
437
438        match self.location {
439            AppendLocation::Start => {}
440            AppendLocation::End => {
441                properties.insert("location".to_owned(), "end".into());
442            }
443        }
444
445        match &self.text_content {
446            TextContent::None => {}
447            TextContent::Value(value) => {
448                properties.insert("text".to_owned(), value.into());
449            }
450            TextContent::FilePath(file_path) => {
451                properties.insert(
452                    "file".to_owned(),
453                    file_path.to_string_lossy().to_string().into(),
454                );
455            }
456        }
457
458        properties
459    }
460}
461
462#[derive(Debug, PartialEq, Eq)]
463enum TextContent {
464    None,
465    Value(String),
466    FilePath(PathBuf),
467}
468
469impl Default for TextContent {
470    fn default() -> Self {
471        Self::None
472    }
473}
474
475#[derive(Debug, PartialEq, Eq)]
476enum AppendLocation {
477    Start,
478    End,
479}
480
481impl AppendLocation {
482    fn write_to_block(&self, block: &mut Block, comment: String) {
483        if let Some(tokens) = block.mutate_tokens() {
484            let final_token = tokens
485                .final_token
486                .get_or_insert_with(|| Token::from_content(""));
487            self.append_comment(final_token, comment);
488        } else {
489            let mut token = Token::from_content("");
490            self.append_comment(&mut token, comment);
491
492            block.set_tokens(BlockTokens {
493                semicolons: Vec::new(),
494                last_semicolon: None,
495                final_token: Some(token),
496            });
497        }
498    }
499
500    fn append_comment(&self, token: &mut Token, comment: String) {
501        match self {
502            AppendLocation::Start => {
503                token.push_leading_trivia(TriviaKind::Comment.with_content(comment));
504            }
505            AppendLocation::End => {
506                token.push_trailing_trivia(TriviaKind::Comment.with_content(comment));
507            }
508        }
509    }
510}
511
512impl Default for AppendLocation {
513    fn default() -> Self {
514        Self::Start
515    }
516}
517
518#[cfg(test)]
519mod test {
520    use super::*;
521    use crate::rules::Rule;
522
523    use insta::assert_json_snapshot;
524
525    #[test]
526    fn serialize_rule_with_text() {
527        let rule: Box<dyn Rule> = Box::new(AppendTextComment::new("content"));
528
529        assert_json_snapshot!("append_text_comment_with_text", rule);
530    }
531
532    #[test]
533    fn serialize_rule_with_text_at_end() {
534        let rule: Box<dyn Rule> = Box::new(AppendTextComment::new("content").at_end());
535
536        assert_json_snapshot!("append_text_comment_with_text_at_end", rule);
537    }
538
539    #[test]
540    fn configure_with_extra_field_error() {
541        let result = json5::from_str::<Box<dyn Rule>>(
542            r#"{
543            rule: 'append_text_comment',
544            text: '',
545            prop: "something",
546        }"#,
547        );
548        pretty_assertions::assert_eq!(result.unwrap_err().to_string(), "unexpected field 'prop'");
549    }
550}