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