1#[cfg(feature = "luau")]
2use full_moon::ast::luau::TypeSpecifier;
3#[cfg(feature = "cfxlua")]
4use full_moon::tokenizer::Symbol;
5use full_moon::tokenizer::{Token, TokenReference};
6use full_moon::{
7 ast::{
8 punctuated::{Pair, Punctuated},
9 Assignment, Call, Expression, FunctionArgs, FunctionCall, LocalAssignment, Suffix,
10 },
11 tokenizer::TokenType,
12};
13
14#[cfg(feature = "lua54")]
15use crate::formatters::lua54::format_attribute;
16#[cfg(feature = "luau")]
17use crate::formatters::luau::format_type_specifier;
18use crate::{
19 context::{create_indent_trivia, create_newline_trivia, Context},
20 fmt_symbol,
21 formatters::{
22 expression::{format_expression, format_var, hang_expression},
23 general::{
24 format_punctuated, format_punctuated_multiline, format_token_reference,
25 try_format_punctuated,
26 },
27 trivia::{
28 strip_leading_trivia, strip_trailing_trivia, strip_trivia, FormatTriviaType,
29 UpdateLeadingTrivia, UpdateTrailingTrivia, UpdateTrivia,
30 },
31 trivia_util::{
32 self, prepend_newline_indent, CommentSearch, GetLeadingTrivia, GetTrailingTrivia,
33 HasInlineComments,
34 },
35 },
36 shape::Shape,
37};
38
39pub fn calculate_hang_level(expression: &Expression) -> Option<usize> {
43 match expression {
44 Expression::Parentheses { .. } => None,
45 Expression::UnaryOperator { expression, .. } => calculate_hang_level(expression),
46 #[cfg(feature = "luau")]
47 Expression::TypeAssertion { expression, .. } => calculate_hang_level(expression),
48 _ => Some(1),
49 }
50}
51
52pub fn hang_punctuated_list(
56 ctx: &Context,
57 punctuated: &Punctuated<Expression>,
58 shape: Shape,
59) -> Punctuated<Expression> {
60 assert!(punctuated.len() == 1);
63
64 let mut output = Punctuated::new();
65
66 for (idx, pair) in punctuated.pairs().enumerate() {
69 let mut value =
79 hang_expression(ctx, pair.value(), shape, calculate_hang_level(pair.value()));
80 if idx != 0 {
81 value =
82 value.update_leading_trivia(FormatTriviaType::Append(vec![create_indent_trivia(
83 ctx, shape,
84 )]));
85 }
86
87 output.push(Pair::new(
88 value,
89 pair.punctuation().map(|x| {
90 fmt_symbol!(ctx, x, ",", shape).update_trailing_trivia(FormatTriviaType::Append(
91 vec![create_newline_trivia(ctx)],
92 ))
93 }),
94 ));
95 }
96
97 output
98}
99
100pub fn hang_equal_token(
103 ctx: &Context,
104 equal_token: &TokenReference,
105 shape: Shape,
106 indent_first_item: bool,
107) -> TokenReference {
108 let mut equal_token_trailing_trivia = vec![create_newline_trivia(ctx)];
109 if indent_first_item {
110 equal_token_trailing_trivia.push(create_indent_trivia(
111 ctx,
112 shape.increment_additional_indent(),
113 ))
114 }
115
116 let equal_token_trailing_trivia = equal_token
117 .trailing_trivia()
118 .filter(|x| trivia_util::trivia_is_comment(x))
119 .flat_map(|x| vec![Token::new(TokenType::spaces(1)), x.to_owned()])
120 .chain(equal_token_trailing_trivia.iter().map(|x| x.to_owned()))
121 .collect();
122
123 equal_token.update_trailing_trivia(FormatTriviaType::Replace(equal_token_trailing_trivia))
124}
125
126fn is_complex_function_call(function_call: &FunctionCall) -> bool {
127 let test_function_args = |function_args: &FunctionArgs| match function_args {
128 FunctionArgs::Parentheses { arguments, .. } => {
129 let mut complexity_count = 0;
130
131 for argument in arguments {
132 match argument {
133 Expression::Function(_) => return true,
134 Expression::TableConstructor(_) => complexity_count += 1,
135 _ => (),
137 }
138 }
139
140 complexity_count > 1
141 }
142 _ => false,
143 };
144
145 function_call.suffixes().any(|suffix| match suffix {
146 Suffix::Call(Call::AnonymousCall(function_args)) => test_function_args(function_args),
147 Suffix::Call(Call::MethodCall(method_call)) => test_function_args(method_call.args()),
148 _ => false,
149 })
150}
151
152fn prevent_equals_hanging(expression: &Expression) -> bool {
154 match expression {
155 Expression::Function(_) => true,
156 Expression::FunctionCall(function_call) => is_complex_function_call(function_call),
157 #[cfg(feature = "luau")]
158 Expression::IfExpression(_) => true,
159 #[cfg(feature = "luau")]
160 Expression::TypeAssertion { expression, .. } => prevent_equals_hanging(expression),
161 _ => false,
162 }
163}
164
165pub fn hang_at_equals_due_to_comments(
166 ctx: &Context,
167 equal_token: &TokenReference,
168 expression: &Expression,
169 shape: Shape,
170) -> (TokenReference, Expression) {
171 let equal_token = hang_equal_token(ctx, equal_token, shape, false);
173
174 let shape = shape.reset().increment_additional_indent();
175
176 let expression = if strip_trivia(expression).has_inline_comments() {
181 hang_expression(ctx, expression, shape, None)
182 } else {
183 format_expression(ctx, expression, shape)
184 };
185
186 let (expression, leading_comments) = trivia_util::take_leading_comments(&expression);
188
189 let leading_comments = leading_comments
191 .iter()
192 .flat_map(|x| {
193 vec![
194 create_indent_trivia(ctx, shape),
195 x.to_owned(),
196 create_newline_trivia(ctx),
197 ]
198 })
199 .chain(std::iter::once(create_indent_trivia(ctx, shape)))
200 .collect();
201
202 let expression = expression.update_leading_trivia(FormatTriviaType::Replace(leading_comments));
203
204 (equal_token, expression)
205}
206
207fn attempt_assignment_tactics(
210 ctx: &Context,
211 expressions: &Punctuated<Expression>,
212 shape: Shape,
213 equal_token: TokenReference,
214) -> (Punctuated<Expression>, TokenReference) {
215 if expressions.len() > 1 {
218 let hanging_equal_token = hang_equal_token(ctx, &equal_token, shape, true);
220 let hanging_shape = shape.reset().increment_additional_indent();
221 let expr_list = format_punctuated(
222 ctx,
223 expressions,
224 hanging_shape.with_infinite_width(),
225 format_expression,
226 );
227
228 if trivia_util::punctuated_inline_comments(expressions, true)
229 || hanging_shape
230 .take_first_line(&strip_trivia(&expr_list))
231 .over_budget()
232 {
233 let multiline_expr = format_punctuated_multiline(
236 ctx,
237 expressions,
238 hanging_shape,
239 format_expression,
240 None,
241 );
242
243 let mut output_expr = Punctuated::new();
245
246 for (idx, (formatted, original)) in
247 multiline_expr.into_pairs().zip(expressions).enumerate()
248 {
249 let shape = hanging_shape.reset();
251
252 if formatted.value().has_inline_comments()
253 || shape
254 .take_first_line(&strip_leading_trivia(formatted.value()))
255 .over_budget()
256 {
257 output_expr.push(formatted.map(|_| {
259 let expression =
260 hang_expression(ctx, original, shape, calculate_hang_level(original));
261 if idx == 0 {
262 expression
263 } else {
264 trivia_util::prepend_newline_indent(ctx, &expression, shape)
265 }
266 }))
267 } else {
268 output_expr.push(formatted);
270 }
271 }
272
273 (output_expr, hanging_equal_token)
274 } else {
275 (expr_list, hanging_equal_token)
276 }
277 } else {
278 let expression = expressions.iter().next().unwrap();
280
281 if trivia_util::token_contains_comments(&equal_token)
283 || expression.has_leading_comments(CommentSearch::Single)
284 {
285 let (equal_token, expression) =
286 hang_at_equals_due_to_comments(ctx, &equal_token, expression, shape);
287
288 let expr_list = std::iter::once(Pair::new(expression, None)).collect();
290
291 return (expr_list, equal_token);
292 }
293
294 let expr_list = format_punctuated(ctx, expressions, shape, format_expression);
295 let formatting_shape = shape.take_first_line(&strip_trailing_trivia(&expr_list));
296
297 if trivia_util::can_hang_expression(expression) {
299 let hanging_expr_list = hang_punctuated_list(ctx, expressions, shape);
301 let hanging_shape = shape.take_first_line(&strip_trivia(&hanging_expr_list));
302
303 if expression.has_inline_comments()
304 || hanging_shape.used_width() < formatting_shape.used_width()
305 {
306 (hanging_expr_list, equal_token)
307 } else {
308 (expr_list, equal_token)
310 }
311 } else if prevent_equals_hanging(expression) {
312 (expr_list, equal_token)
313 } else {
314 let hanging_equal_token = hang_equal_token(ctx, &equal_token, shape, true);
316 let equal_token_shape = shape.reset().increment_additional_indent();
317 let hanging_equal_token_expr_list =
318 format_punctuated(ctx, expressions, equal_token_shape, format_expression);
319 let equal_token_shape = equal_token_shape
320 .take_first_line(&strip_trailing_trivia(&hanging_equal_token_expr_list));
321
322 if !equal_token_shape.over_budget()
325 && format!("{hanging_equal_token_expr_list}").lines().count() + 1 < format!("{expr_list}").lines().count()
327 || formatting_shape.over_budget()
328 {
329 (hanging_equal_token_expr_list, hanging_equal_token)
330 } else {
331 (expr_list, equal_token)
332 }
333 }
334 }
335}
336
337pub fn format_assignment_no_trivia(
338 ctx: &Context,
339 assignment: &Assignment,
340 mut shape: Shape,
341) -> Assignment {
342 let contains_comments = trivia_util::token_contains_comments(assignment.equal_token())
345 || trivia_util::punctuated_inline_comments(assignment.expressions(), true);
346
347 let mut var_list = try_format_punctuated(
349 ctx,
350 assignment.variables(),
351 shape.with_infinite_width(),
352 format_var,
353 Some(1),
354 );
355 let mut equal_token = fmt_symbol!(ctx, assignment.equal_token(), " = ", shape);
356 let mut expr_list = format_punctuated(
357 ctx,
358 assignment.expressions(),
359 shape.with_infinite_width(),
360 format_expression,
361 );
362
363 if var_list.has_trailing_comments(trivia_util::CommentSearch::Single) {
365 const EQUAL_TOKEN_LEN: usize = "= ".len();
366 shape = shape
367 .reset()
368 .increment_additional_indent()
369 .add_width(EQUAL_TOKEN_LEN);
370 equal_token = prepend_newline_indent(ctx, &equal_token, shape);
371 }
372
373 let singleline_shape = shape
375 + (strip_leading_trivia(&var_list).to_string().len()
376 + 3
377 + strip_trailing_trivia(&expr_list).to_string().len());
378 if contains_comments || singleline_shape.over_budget() {
379 var_list = try_format_punctuated(ctx, assignment.variables(), shape, format_var, Some(1));
381 let shape = shape + (strip_leading_trivia(&var_list).to_string().len() + 3);
382
383 let (new_expr_list, new_equal_token) =
384 attempt_assignment_tactics(ctx, assignment.expressions(), shape, equal_token);
385 expr_list = new_expr_list;
386 equal_token = new_equal_token;
387 }
388
389 Assignment::new(var_list, expr_list).with_equal_token(equal_token)
390}
391
392pub fn format_assignment(ctx: &Context, assignment: &Assignment, shape: Shape) -> Assignment {
393 let leading_trivia = vec![create_indent_trivia(ctx, shape)];
394 let trailing_trivia = vec![create_newline_trivia(ctx)];
395
396 format_assignment_no_trivia(ctx, assignment, shape).update_trivia(
397 FormatTriviaType::Append(leading_trivia),
398 FormatTriviaType::Append(trailing_trivia),
399 )
400}
401
402fn format_local_no_assignment(
403 ctx: &Context,
404 assignment: &LocalAssignment,
405 shape: Shape,
406) -> LocalAssignment {
407 let local_token = fmt_symbol!(ctx, assignment.local_token(), "local ", shape);
408 let shape = shape + 6; let name_list = try_format_punctuated(
410 ctx,
411 assignment.names(),
412 shape,
413 format_token_reference,
414 Some(1),
415 );
416
417 #[cfg(feature = "lua54")]
418 let attributes = assignment
419 .attributes()
420 .map(|x| x.map(|attribute| format_attribute(ctx, attribute, shape)))
421 .collect();
422
423 #[cfg(feature = "luau")]
424 let type_specifiers: Vec<Option<TypeSpecifier>> = assignment
425 .type_specifiers()
426 .map(|x| x.map(|type_specifier| format_type_specifier(ctx, type_specifier, shape)))
427 .collect();
428
429 let local_assignment = LocalAssignment::new(name_list);
430 #[cfg(feature = "lua54")]
431 let local_assignment = local_assignment.with_attributes(attributes);
432 #[cfg(feature = "luau")]
433 let local_assignment = local_assignment.with_type_specifiers(type_specifiers);
434
435 local_assignment
436 .with_local_token(local_token)
437 .with_equal_token(None)
438 .with_expressions(Punctuated::new())
439}
440
441pub fn format_local_assignment_no_trivia(
442 ctx: &Context,
443 assignment: &LocalAssignment,
444 mut shape: Shape,
445) -> LocalAssignment {
446 if assignment.expressions().is_empty() {
447 format_local_no_assignment(ctx, assignment, shape)
448 } else {
449 let contains_comments = assignment
452 .equal_token()
453 .is_some_and(trivia_util::token_contains_comments)
454 || trivia_util::punctuated_inline_comments(assignment.expressions(), true);
455
456 let local_token = fmt_symbol!(ctx, assignment.local_token(), "local ", shape);
458
459 let mut name_list = try_format_punctuated(
460 ctx,
461 assignment.names(),
462 shape.with_infinite_width(),
463 format_token_reference,
464 Some(1),
465 );
466 let mut equal_token = fmt_symbol!(ctx, assignment.equal_token().unwrap(), " = ", shape);
467
468 #[cfg(feature = "cfxlua")]
470 if let TokenType::Symbol { symbol: Symbol::In } =
471 assignment.equal_token().unwrap().token_type()
472 {
473 equal_token = fmt_symbol!(ctx, assignment.equal_token().unwrap(), " in ", shape);
474 }
475
476 let mut expr_list = format_punctuated(
477 ctx,
478 assignment.expressions(),
479 shape.with_infinite_width(),
480 format_expression,
481 );
482
483 #[cfg(feature = "lua54")]
484 let attributes: Vec<Option<_>> = assignment
485 .attributes()
486 .map(|x| x.map(|attribute| format_attribute(ctx, attribute, shape)))
487 .collect();
488
489 #[cfg(feature = "luau")]
490 let type_specifiers: Vec<Option<TypeSpecifier>> = assignment
491 .type_specifiers()
492 .map(|x| x.map(|type_specifier| format_type_specifier(ctx, type_specifier, shape)))
493 .collect();
494
495 #[allow(unused_mut)]
496 let mut type_specifier_len = 0;
497 #[cfg(feature = "lua54")]
498 {
499 type_specifier_len += attributes.iter().fold(0, |acc, x| {
500 acc + x.as_ref().map_or(0, |y| y.to_string().len())
501 });
502 }
503 #[cfg(feature = "luau")]
504 {
505 type_specifier_len += type_specifiers.iter().fold(0, |acc, x| {
506 acc + x.as_ref().map_or(0, |y| y.to_string().len())
507 });
508 }
509
510 #[cfg(feature = "luau")]
511 let var_list_ends_with_comments = match type_specifiers.last() {
512 Some(Some(specifier)) => {
513 specifier.has_trailing_comments(trivia_util::CommentSearch::Single)
514 }
515 _ => name_list.has_trailing_comments(trivia_util::CommentSearch::Single),
516 };
517 #[cfg(not(feature = "luau"))]
518 let var_list_ends_with_comments =
519 name_list.has_trailing_comments(trivia_util::CommentSearch::Single);
520
521 if var_list_ends_with_comments {
523 const EQUAL_TOKEN_LEN: usize = "= ".len();
524 shape = shape
525 .reset()
526 .increment_additional_indent()
527 .add_width(EQUAL_TOKEN_LEN);
528 equal_token = prepend_newline_indent(ctx, &equal_token, shape);
529 }
530
531 let singleline_shape = shape
533 + (strip_leading_trivia(&name_list).to_string().len()
534 + 6 + 3 + type_specifier_len
537 + strip_trailing_trivia(&expr_list).to_string().len());
538
539 if contains_comments || singleline_shape.over_budget() {
540 name_list = try_format_punctuated(
542 ctx,
543 assignment.names(),
544 shape,
545 format_token_reference,
546 Some(1),
547 );
548 let shape = shape
549 + (strip_leading_trivia(&name_list).to_string().len() + 6 + 3 + type_specifier_len);
550
551 let (new_expr_list, new_equal_token) =
552 attempt_assignment_tactics(ctx, assignment.expressions(), shape, equal_token);
553 expr_list = new_expr_list;
554 equal_token = new_equal_token;
555 }
556
557 let local_assignment = LocalAssignment::new(name_list);
558 #[cfg(feature = "lua54")]
559 let local_assignment = local_assignment.with_attributes(attributes);
560 #[cfg(feature = "luau")]
561 let local_assignment = local_assignment.with_type_specifiers(type_specifiers);
562 local_assignment
563 .with_local_token(local_token)
564 .with_equal_token(Some(equal_token))
565 .with_expressions(expr_list)
566 }
567}
568
569pub fn format_local_assignment(
570 ctx: &Context,
571 assignment: &LocalAssignment,
572 shape: Shape,
573) -> LocalAssignment {
574 let leading_trivia = vec![create_indent_trivia(ctx, shape)];
576 let trailing_trivia = vec![create_newline_trivia(ctx)];
577
578 format_local_assignment_no_trivia(ctx, assignment, shape).update_trivia(
579 FormatTriviaType::Append(leading_trivia),
580 FormatTriviaType::Append(trailing_trivia),
581 )
582}