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