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#[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}