diff --git a/src/classifier.rs b/src/classifier.rs index 299946f..bb758ba 100644 --- a/src/classifier.rs +++ b/src/classifier.rs @@ -377,8 +377,9 @@ fn classify_fortran(trimmed: &str) -> LineKind { return LineKind::FortranStatement; } // type is (...) was already handled in continuations above - // type :: name, type::name, or type, attrs :: name -> definition (block open) - let re_type_def = regex!(r"(?i)^type\s*(::|,|\s)"); + // type :: name, type::name, type name, or type, attrs :: name -> definition + // (block open). `type = 0` is an assignment to a variable named type. + let re_type_def = regex!(r"(?i)^type\s*(::|,|\s+\w)"); if re_type_def.is_match(line) { return LineKind::FortranBlockOpen; } diff --git a/src/formatter.rs b/src/formatter.rs index 37c4b6c..eb28795 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -86,6 +86,16 @@ pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, u source }; let logical_lines = read_logical_lines(src); + let logical_lines = if range.is_none() { + let lines = lift_hoisted_comments(logical_lines); + if config.split_statements.is_enabled() { + split_logical_statements(lines) + } else { + lines + } + } else { + logical_lines + }; let mut tracker = ScopeTracker::new(); let mut output_lines: Vec = Vec::new(); // Output index intervals emitted verbatim for out-of-range logical lines @@ -217,6 +227,7 @@ pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, u None }; + let comment_depth = tracker.current_depth(); let mut depth = tracker.process_with_name(kind, scope_name); // Track procedure exit @@ -465,7 +476,12 @@ pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, u // continuation line silently disables it. let wrapped = if config.rewrap_code.is_enabled() && kind != LineKind::Directive { - rewrap_line(&formatted, config.line_length, config.indent_width) + rewrap_line_at( + &formatted, + config.line_length, + config.indent_width, + Some(comment_depth * config.indent_width), + ) } else { vec![formatted.clone()] }; @@ -553,7 +569,12 @@ pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, u }; let wrapped = if config.rewrap_code.is_enabled() { - rewrap_line(&formatted, config.line_length, config.indent_width) + rewrap_line_at( + &formatted, + config.line_length, + config.indent_width, + Some(comment_depth * config.indent_width), + ) } else { vec![formatted.clone()] }; @@ -658,6 +679,10 @@ pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, u let ll = config.line_length; output_lines = apply(output_lines, &|lines| rewrap_inline_doxygen(lines, ll)); } + // Consuming `!!` lines can leave blank lines adjacent. Collapse them before + // compaction and alignment, which treat a single blank line differently + // from two. + output_lines = apply(output_lines, &collapse_blank_lines); // Compact use statements (after !< rewrap, since rewrap may consume !! lines) if config.compact_use.is_enabled() { output_lines = apply(output_lines, &compact_use_statements); @@ -714,19 +739,7 @@ pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, u output_lines = apply(output_lines, &strip_trailing_semicolons); // Collapse consecutive blank lines that may result from semicolon stripping - output_lines = apply(output_lines, &|lines: &[String]| { - let mut collapsed = Vec::with_capacity(lines.len()); - let mut prev_blank = false; - for line in lines { - let is_blank = line.trim().is_empty(); - if is_blank && prev_blank { - continue; - } - prev_blank = is_blank; - collapsed.push(line.clone()); - } - collapsed - }); + output_lines = apply(output_lines, &collapse_blank_lines); // Modernize legacy relational operators (.eq. -> ==, etc.) if config.modernize_operators.is_enabled() { @@ -775,6 +788,150 @@ pub fn format_with_config(source: &str, config: &Config, range: Option<(usize, u /// Check if a line is a `! ffmt off` or `! ffmt on` marker. /// Returns Some(true) for off, Some(false) for on, None otherwise. +/// Turn the comments hoisted out of a continuation into ordinary comment lines +/// ahead of their statement. They then take part in the same block merging and +/// reflow as written comments; emitting them separately left output that a +/// second run merged and reflowed. Statements that are emitted verbatim keep +/// their comments in place and are left alone. +fn lift_hoisted_comments(lines: Vec) -> Vec { + let mut out = Vec::with_capacity(lines.len()); + for i in 0..lines.len() { + let ll = &lines[i]; + if ll.hoisted_comments.is_empty() || ll.preserve { + out.push(ll.clone()); + continue; + } + let interrupted = lines.get(i + 1).is_some_and(|next| { + matches!( + classify(&next.joined), + LineKind::PreprocessorDirective + | LineKind::PreprocessorContinuation + | LineKind::PreprocessorClose + | LineKind::FyppBlockOpen + | LineKind::FyppBlockClose + | LineKind::FyppContinuation + | LineKind::FyppStatement + ) + }) && ll + .raw_lines + .last() + .is_some_and(|l| l.trim_end().ends_with('&')); + if interrupted || classify(&ll.joined) == LineKind::InlineFypp { + out.push(ll.clone()); + continue; + } + for comment in &ll.hoisted_comments { + out.push(LogicalLine { + joined: comment.clone(), + raw_lines: vec![comment.clone()], + line_number: ll.line_number, + hoisted_comments: Vec::new(), + preserve: false, + }); + } + let mut stmt = ll.clone(); + stmt.hoisted_comments.clear(); + out.push(stmt); + } + out +} + +/// Split `a; b; c` into one logical line per statement before anything else +/// looks at it. Splitting only the finished output left the scope tracker and +/// the wrapper seeing one long block-opening line, so the pieces were indented +/// and wrapped as if they were a single statement and a second run changed them. +/// A trailing comment stays with the last statement. `private; public ::` is +/// kept as written. +fn split_logical_statements(lines: Vec) -> Vec { + let mut out = Vec::with_capacity(lines.len()); + for ll in lines { + let kind = classify(&ll.joined); + let skip = ll.preserve + || matches!( + kind, + LineKind::Blank + | LineKind::Comment + | LineKind::Directive + | LineKind::PreprocessorDirective + | LineKind::PreprocessorContinuation + | LineKind::PreprocessorClose + | LineKind::FyppStatement + | LineKind::FyppBlockOpen + | LineKind::FyppBlockClose + | LineKind::FyppContinuation + | LineKind::InlineFypp + ) + || ll.joined.trim_start().starts_with('!'); + if skip { + out.push(ll); + continue; + } + let (code, comment) = split_trailing_comment(&ll.joined); + let lower = code.trim_start().to_ascii_lowercase(); + if lower.starts_with("private") && lower.contains("; public") { + out.push(ll); + continue; + } + let mut cuts: Vec = Vec::new(); + let mut quote: Option = None; + let bytes = code.as_bytes(); + let mut i = 0; + while i < bytes.len() { + let b = bytes[i]; + match quote { + Some(q) => { + if b == q { + if i + 1 < bytes.len() && bytes[i + 1] == q { + i += 1; // escaped quote + } else { + quote = None; + } + } + } + None => match b { + b'\'' | b'"' => quote = Some(b), + b';' => cuts.push(i), + _ => {} + }, + } + i += 1; + } + if cuts.is_empty() { + out.push(ll); + continue; + } + let mut pieces: Vec = Vec::new(); + let mut start = 0; + for &c in &cuts { + pieces.push(code[start..c].trim().to_string()); + start = c + 1; + } + pieces.push(code[start..].trim().to_string()); + pieces.retain(|p| !p.is_empty()); + let Some(last) = pieces.len().checked_sub(1) else { + out.push(ll); + continue; + }; + if !comment.is_empty() { + pieces[last] = format!("{} {}", pieces[last], comment.trim_end()); + } + for (n, piece) in pieces.into_iter().enumerate() { + out.push(LogicalLine { + joined: piece.clone(), + raw_lines: vec![piece], + line_number: ll.line_number, + hoisted_comments: if n == 0 { + ll.hoisted_comments.clone() + } else { + Vec::new() + }, + preserve: false, + }); + } + } + out +} + /// Emit comments hoisted out of a `&` continuation as full-line comments /// above the statement, at the statement's indent. fn emit_hoisted_comments( @@ -909,6 +1066,16 @@ fn process_line(line: &str, config: &Config) -> String { result = normalize_whitespace(&result, &config.whitespace); + // Modernize before any rewrap: `.lt.` -> `<` shortens the line, and wrapping + // the longer form makes a second run wrap differently. + if config.modernize_operators.is_enabled() { + result = modernize_relational_operators(&[result]).remove(0); + } + + if config.whitespace.comma.is_enabled() { + result = space_top_level_commas(&result); + } + if config.keyword_paren_space.is_enabled() { result = crate::whitespace::add_keyword_paren_spaces(&result); } @@ -924,6 +1091,11 @@ fn process_line(line: &str, config: &Config) -> String { KeywordCase::Preserve => {} } + // Dropped here, not only in the final pass, so a line is wrapped at the + // length it will have (the `;` would otherwise force a break that a second + // run, seeing no `;`, undoes). + result = strip_trailing_semicolons(&[result]).remove(0); + if config.unicode_to_ascii { // Standalone comments are handled in the Comment branch; inline // trailing comments are converted here (code text is left alone — @@ -937,6 +1109,51 @@ fn process_line(line: &str, config: &Config) -> String { result } +/// Put a space after every comma outside parentheses, brackets, braces and +/// strings. The whitespace pass already does this except in statements such as +/// `namelist`, where the item list is not tokenized. When such a list is +/// wrapped, the break comma gains a space as the lines are joined again, so +/// leaving the others bare makes the next run measure (and wrap) different text. +fn space_top_level_commas(line: &str) -> String { + let trimmed = line.trim_start(); + if trimmed.starts_with('!') + || trimmed.starts_with('#') + || trimmed.starts_with("$:") + || trimmed.starts_with("@:") + || !line.contains(',') + { + return line.to_string(); + } + let mut out = String::with_capacity(line.len() + 8); + let mut depth = 0usize; + let mut quote: Option = None; + let mut chars = line.chars().peekable(); + while let Some(c) = chars.next() { + out.push(c); + match quote { + Some(q) => { + if c == q { + quote = None; + } + } + None => match c { + '\'' | '"' => quote = Some(c), + '!' => { + out.extend(chars); + return out; + } + '(' | '[' | '{' => depth += 1, + ')' | ']' | '}' => depth = depth.saturating_sub(1), + ',' if depth == 0 && chars.peek().is_some_and(|n| !n.is_whitespace()) => { + out.push(' '); + } + _ => {} + }, + } + } + out +} + /// Count leading spaces in a string. fn leading_spaces(s: &str) -> usize { s.len() - s.trim_start().len() @@ -1635,8 +1852,7 @@ fn remove_blanks_before_closers(lines: &[String]) -> Vec { // Remove blank lines after openers if prev_was_opener && trimmed.is_empty() { - prev_was_opener = false; - continue; // skip this blank line + continue; // skip every blank in the run, not just the first } // Only EXECUTABLE construct openers (if/do/select/block/...) remove a @@ -2034,6 +2250,21 @@ fn strip_trailing_semicolons(lines: &[String]) -> Vec { /// Ensure a blank line between the last declaration and the first executable statement /// within procedure bodies. Only acts when there is no blank line already. +/// The statement starting at `lines[start]`, with its `&` continuation lines +/// joined, and the number of continuation lines that follow `start`. +fn continued_statement(lines: &[String], start: usize) -> (String, usize) { + let mut statement = lines[start].trim().to_string(); + let mut extra = 0; + while statement.ends_with('&') && start + extra + 1 < lines.len() { + statement.pop(); + extra += 1; + let next = lines[start + extra].trim(); + statement.push(' '); + statement.push_str(next.strip_prefix('&').unwrap_or(next).trim_start()); + } + (statement, extra) +} + fn separate_declarations_from_code(lines: &[String]) -> Vec { let mut result: Vec = Vec::with_capacity(lines.len()); // Track whether we're in a declaration region at the top of a procedure @@ -2043,7 +2274,10 @@ fn separate_declarations_from_code(lines: &[String]) -> Vec { r"(?i)^(?:(?:pure|elemental|impure|recursive|module|integer|real|double\s+precision|complex|character|logical|type\s*\([^)]*\))\s+)*(subroutine|function)\b" ); - for line in lines { + let mut i = 0; + while i < lines.len() { + let line = &lines[i]; + i += 1; let trimmed = line.trim(); let lower = trimmed.to_ascii_lowercase(); @@ -2097,9 +2331,14 @@ fn separate_declarations_from_code(lines: &[String]) -> Vec { continue; } - if is_declaration_line(line) { + // A wrapped declaration puts its `::` on a later line, so judge the + // whole statement and keep its continuation lines together. + let (statement, extra) = continued_statement(lines, i - 1); + if is_declaration_line(&statement) { saw_declaration = true; result.push(line.clone()); + result.extend(lines[i..i + extra].iter().cloned()); + i += extra; continue; } @@ -2208,9 +2447,9 @@ fn rewrap_fypp_line(line: &str, max_length: usize) -> Vec { for &comma_end in &comma_positions { // +2 accounts for trailing " &" on non-last lines let current_len = if result.is_empty() { - indent + comma_end + 2 + indent + comma_end + comma_gap(bytes, comma_end) + 2 } else { - cont_with_amp.len() + (comma_end - chunk_start) + 2 + cont_with_amp.len() + (comma_end - chunk_start) + comma_gap(bytes, comma_end) + 2 }; if current_len > max_length && chunk_start < comma_end { @@ -2221,9 +2460,9 @@ fn rewrap_fypp_line(line: &str, max_length: usize) -> Vec { continue; } let len_with_break = if result.is_empty() { - indent + cp + 2 + indent + cp + comma_gap(bytes, cp) + 2 } else { - cont_with_amp.len() + (cp - chunk_start) + 2 + cont_with_amp.len() + (cp - chunk_start) + comma_gap(bytes, cp) + 2 }; if len_with_break <= max_length { best_break = cp; @@ -2266,13 +2505,30 @@ fn rewrap_fypp_line(line: &str, max_length: usize) -> Vec { /// Returns a Vec of output lines. If the input fits within `max_length`, /// returns it unchanged as a single-element Vec. fn rewrap_line(line: &str, max_length: usize, indent_width: usize) -> Vec { + rewrap_line_at(line, max_length, indent_width, None) +} + +/// `rewrap_line`, with the indent for a trailing comment moved above the code. +/// A standalone comment takes the depth of the enclosing block, which differs +/// from the statement's own indent on `end do`, `else` and similar lines; +/// using the statement's indent would make a second run re-indent it. +fn rewrap_line_at( + line: &str, + max_length: usize, + indent_width: usize, + comment_indent: Option, +) -> Vec { if line.len() <= max_length || max_length >= 1000 { return vec![line.to_string()]; } // Full-line comments and directives never belong here: relocating or // wrapping them corrupts the marker (`!$acc` would become a comment). - if line.trim_start().starts_with('!') { + // Preprocessor and Fypp directives (`#:set x = a != b`) are not Fortran + // either: the `!=` would be taken for a trailing comment and moved above + // its statement. + let first = line.trim_start(); + if first.starts_with('!') || first.starts_with('#') { return vec![line.to_string()]; } @@ -2292,7 +2548,8 @@ fn rewrap_line(line: &str, max_length: usize, indent_width: usize) -> Vec Vec abs_limit { + if bp + comma_gap(content.as_bytes(), bp) > abs_limit { break; } match kind { @@ -2644,7 +2901,12 @@ fn find_token_breaks(content: &str) -> Vec<(usize, BreakKind)> { // Never break immediately before `%` (member accessor). A break there // would produce `& %member` on the next line, which is a blank within a // lexical token — invalid in Fortran free-form (§6.3.2.2). - breaks.retain(|&(bp, _)| bp >= len || bytes[bp] != b'%'); + // Spaces between the break and the `%` count: re-joining the lines puts + // one there, and the text must read the same on the next run. + breaks.retain(|&(bp, _)| { + let next = bytes[bp.min(len)..].iter().find(|&&b| b != b' '); + next != Some(&b'%') + }); breaks } @@ -2983,6 +3245,28 @@ fn split_multi_statements(lines: &[String], indent_width: usize) -> Vec result } +/// 1 when a break at `bp` follows a comma that is not followed by a space. +/// Joining the continuation again puts a space there, so a break is measured as +/// if the space were already present; text with and without it then wraps alike. +fn comma_gap(bytes: &[u8], bp: usize) -> usize { + usize::from(bp > 0 && bp < bytes.len() && bytes[bp - 1] == b',' && bytes[bp] != b' ') +} + +/// Collapse runs of blank lines to one. +fn collapse_blank_lines(lines: &[String]) -> Vec { + let mut collapsed = Vec::with_capacity(lines.len()); + let mut prev_blank = false; + for line in lines { + let is_blank = line.trim().is_empty(); + if is_blank && prev_blank { + continue; + } + prev_blank = is_blank; + collapsed.push(line.clone()); + } + collapsed +} + /// Align trailing `&` continuation markers at the column limit. /// Lines ending with ` &` are padded so `&` sits at the column limit position. fn align_ampersands(lines: &[String], col_limit: usize) -> Vec { @@ -3052,7 +3336,14 @@ fn align_assignments(lines: &[String], max_length: usize) -> Vec { ); let indent = leading_spaces(&lines[i]); let same_indent = group_indent.is_none_or(|gi| gi == indent); - if !is_control && same_indent { + // Lines of a continued statement (including Fypp macro keyword + // arguments such as `& collapse=3)`) and preprocessor lines are + // not assignments; padding their `=` changes them on the next run. + let trimmed = lines[i].trim(); + let is_continuation_part = trimmed.starts_with('&') || trimmed.ends_with('&'); + let is_directive = + trimmed.starts_with('#') || trimmed.starts_with("$:") || trimmed.starts_with("@:"); + if !is_control && same_indent && !is_continuation_part && !is_directive { if let Some(eq_col) = find_assignment_eq(&lines[i]) { group.push(i); eq_columns.push(eq_col); diff --git a/tests/bugfixes.rs b/tests/bugfixes.rs index be3de29..fec7a87 100644 --- a/tests/bugfixes.rs +++ b/tests/bugfixes.rs @@ -995,3 +995,175 @@ fn test_overflow_still_absorbs_ordinary_prose() { "overflow did not reflow into the following prose line:\n{out}" ); } + +// --- Idempotence: operators are modernized before wrapping --- + +#[test] +fn test_modernized_operators_wrap_is_idempotent() { + // `.lt.` -> `<` shortens the statement; wrapping the longer legacy form + // used to leave a layout that a second run re-wrapped. + let src = "program t\n\ + if(umask(i-1,j,k).lt.0.5d0.and.umask(i,j,k).lt.0.5d0.and. &\n\ + vmask(i,j-1,k).lt.0.5d0.and.vmask(i,j,k).lt.0.5d0.and. &\n\ + wmask(i,j,k-1).lt.0.5d0.and.wmask(i,j,k).lt.0.5d0) then ! in solid\n\ + x = 1\n\ + end if\nend program t\n"; + let config = Config { + line_length: 100, + ..Config::default() + }; + let once = ffmt::format_string_with_config(src, &config); + let twice = ffmt::format_string_with_config(&once, &config); + assert_eq!(once, twice, "second run changed the output:\n{once}"); +} + +// --- Idempotence: a second run must not change the first run's output --- + +fn assert_idempotent(src: &str, config: &Config) { + let once = ffmt::format_string_with_config(src, config); + let twice = ffmt::format_string_with_config(&once, config); + assert_eq!(once, twice, "second run changed the output:\n{once}"); +} + +fn with_line_length(line_length: usize) -> Config { + Config { + line_length, + ..Config::default() + } +} + +#[test] +fn test_idempotent_comment_moved_above_block_close() { + // A trailing comment moved above `end do` / `else` belongs at the depth of + // the block body, as a written comment there would be. + let src = "program t\ndo i = 1, n\n x = 1\n end do !... i = patch%offset(1) + 1, patch%offset(1) + patch%localSize(1)\n\ + if (a) then\n y = 2\n else !... something quite long that goes past the limit of sixty chars ok\n y = 3\n end if\nend program t\n"; + assert_idempotent(src, &with_line_length(60)); +} + +#[test] +fn test_idempotent_blank_run_after_block_opener() { + let src = + "program t\n do i = 1, n\n\n\n ! Create X\n x = 1\n end do\nend program t\n"; + let out = ffmt::format_string(src); + assert!( + !out.contains("do i = 1, n\n\n"), + "blank lines after an opener should all go:\n{out}" + ); + assert_idempotent(src, &Config::default()); +} + +#[test] +fn test_variable_named_type_is_not_a_block_opener() { + let out = ffmt::format_string("program t\nif (a) then\ntype=0\nend if\nend program t\n"); + assert!( + out.contains(" end if"), + "`type = 0` must not open a block:\n{out}" + ); + assert_idempotent( + "program t\nif (a) then\ntype=0\nend if\nend program t\n", + &Config::default(), + ); +} + +#[test] +fn test_idempotent_split_statements_on_long_one_liner() { + let src = "program t\n do i=1,Nxt; s=dfloat(i-Ng)/dfloat(Nx); xh(i)=xLength*(xform*s*(0.5d0-s)*(1d0-s)+s); enddo\nend program t\n"; + let config = Config { + split_statements: ffmt::config::Toggle::Enable, + ..with_line_length(100) + }; + let out = ffmt::format_string_with_config(src, &config); + assert!( + out.contains(" do i = 1, Nxt\n s = dfloat(i - Ng)/dfloat(Nx)\n"), + "statements should be split and indented as a block:\n{out}" + ); + assert_idempotent(src, &config); +} + +#[test] +fn test_idempotent_fypp_macro_overflowing_after_last_comma() { + let src = "module m\ncontains\nsubroutine s\nif (a) then\nif (a) then\nif (a) then\n$:GPU_PARALLEL_LOOP(collapse=4,private='[i, j, k, l, slopeL, slopeR, slope]')\nend if\nend if\nend if\nend subroutine s\nend module m\n"; + assert_idempotent(src, &with_line_length(90)); +} + +#[test] +fn test_fypp_directive_with_python_not_equal_is_not_split() { + // `!=` in a `#:set` line is Python, not the start of a Fortran comment. + let line = + " #:set emitted_clause = clause_val.strip() != '' or extraOmpArgs_val.strip() != ''"; + let src = format!("#:def m()\n{line}\n#:enddef\n"); + let out = ffmt::format_string_with_config(&src, &with_line_length(80)); + assert!( + out.contains(line.trim_start()), + "directive was altered:\n{out}" + ); +} + +// Inputs taken from real code that a second run used to change. The first two +// are long fixtures, so they live under tests/fixtures/idempotence/. + +#[test] +fn test_idempotent_blank_lines_after_consumed_doxygen_continuation() { + // `!<` + `!!` pairs are merged, leaving adjacent blank lines that must + // collapse before declarations are compacted. + let src = include_str!("fixtures/idempotence/consumed_doxygen_blanks.f90"); + assert_idempotent(src, &with_line_length(100)); +} + +#[test] +fn test_idempotent_namelist_wrap_with_bare_commas() { + // Bare commas gain a space where a line breaks; the rest must match. + let src = include_str!("fixtures/idempotence/namelist_bare_commas.f90"); + assert_idempotent(src, &with_line_length(100)); +} + +#[test] +fn test_idempotent_hoisted_continuation_comment() { + // A comment inside a continuation is hoisted above the statement and must + // merge with the comment before it exactly as written comments do. + let src = include_str!("fixtures/idempotence/hoisted_continuation_comment.f90"); + assert_idempotent(src, &with_line_length(80)); +} + +#[test] +fn test_idempotent_wrapped_declaration_after_doc_comment() { + // Alignment lengthens the declaration after `!<` is moved above it; the + // wrapped result must still count as a declaration. + let src = include_str!("fixtures/idempotence/wrapped_declaration.f90"); + assert_idempotent(src, &with_line_length(60)); +} + +#[test] +fn test_idempotent_trailing_semicolon_on_long_line() { + let src = include_str!("fixtures/idempotence/trailing_semicolon.f90"); + let config = Config { + indent_width: 2, + ..with_line_length(90) + }; + assert_idempotent(src, &config); +} + +#[test] +fn test_idempotent_wrap_with_space_before_member_access() { + // Source left by an older ffmt reads `ghost_points(q) %ip_loc`; a break + // between them must not be offered just because a space precedes the `%`. + let src = include_str!("fixtures/idempotence/break_before_member_access.f90"); + assert_idempotent(src, &with_line_length(80)); +} + +#[test] +fn test_align_assignments_leaves_continuation_arguments_alone() { + // `& no_create=None, ...` is a macro keyword argument, not an assignment. + let src = include_str!("fixtures/idempotence/fypp_def_keyword_arguments.fpp"); + let config = Config { + align_assignments: ffmt::config::Toggle::Enable, + ..with_line_length(100) + }; + let out = ffmt::format_string_with_config(src, &config); + assert!( + !out.contains("no_create ="), + "continuation argument was padded:\n{out}" + ); + assert_idempotent(src, &config); +} diff --git a/tests/fixtures/idempotence/break_before_member_access.f90 b/tests/fixtures/idempotence/break_before_member_access.f90 new file mode 100644 index 0000000..05eec3e --- /dev/null +++ b/tests/fixtures/idempotence/break_before_member_access.f90 @@ -0,0 +1,12 @@ +program t +if (a) then +if (b) then +if (c) then +if (d) then +print *, "Image point is located approximately ", & + & (ghost_points(q)%loc(dim) - ghost_points(q) %ip_loc(dim))/(s_cc(1) - s_cc(0)), " grid cells away" +end if +end if +end if +end if +end program t diff --git a/tests/fixtures/idempotence/consumed_doxygen_blanks.f90 b/tests/fixtures/idempotence/consumed_doxygen_blanks.f90 new file mode 100644 index 0000000..a89646e --- /dev/null +++ b/tests/fixtures/idempotence/consumed_doxygen_blanks.f90 @@ -0,0 +1,28 @@ +module m + + + IMPLICIT NONE + + + + TYPE(vector_field), ALLOCATABLE, DIMENSION(:) :: q_cons_ts !< + !! Cell-average conservative variables at each time-stage (TS) + + TYPE(scalar_field), PRIVATE, ALLOCATABLE, DIMENSION(:) :: q_prim_vf !< + !! Cell-average primitive variables at the current time-stage + + + TYPE(scalar_field), ALLOCATABLE, DIMENSION(:) :: rhs_vf !< + !! Cell-average RHS variables at the current time-stage + + + TYPE(vector_field), ALLOCATABLE, DIMENSION(:) :: q_prim_ts !< + !! Cell-average primitive variables at consecutive TIMESTEPS + + + INTEGER, PRIVATE :: num_ts !< + !! Number of time stages in the time-stepping scheme + + + CONTAINS +end module m diff --git a/tests/fixtures/idempotence/fypp_def_keyword_arguments.fpp b/tests/fixtures/idempotence/fypp_def_keyword_arguments.fpp new file mode 100644 index 0000000..db9e04d --- /dev/null +++ b/tests/fixtures/idempotence/fypp_def_keyword_arguments.fpp @@ -0,0 +1,5 @@ +#:def ACC_PARALLEL(code, private=None, default='present', firstprivate=None, reduction=None, reductionOp=None, & + & copy=None, copyin=None, copyinReadOnly=None, copyout=None, create=None, & + & no_create=None, present=None, deviceptr=None, attach=None, extraAccArgs=None) + #:set x = 1 +#:enddef diff --git a/tests/fixtures/idempotence/hoisted_continuation_comment.f90 b/tests/fixtures/idempotence/hoisted_continuation_comment.f90 new file mode 100644 index 0000000..e4e18be --- /dev/null +++ b/tests/fixtures/idempotence/hoisted_continuation_comment.f90 @@ -0,0 +1,10 @@ +module m + implicit none + + ! Test mode + integer, parameter :: & + NULL = 0, & + DERIVATIVE = 1, & + !DERIVATIVE_BC = 2, & + FLUX = 3 +end module m diff --git a/tests/fixtures/idempotence/namelist_bare_commas.f90 b/tests/fixtures/idempotence/namelist_bare_commas.f90 new file mode 100644 index 0000000..ed20f5d --- /dev/null +++ b/tests/fixtures/idempotence/namelist_bare_commas.f90 @@ -0,0 +1,21 @@ +module m +contains + subroutine s + namelist /lppparameters/ DropStatisticsMethod, dragmodel, nTimeStepTag, & + DoConvertVOF2LPP,DoConvertLPP2VOF,CriteriaConvertCase, ConvertRegSizeToDiam, & + vol_cut,xlpp_min,xlpp_max,ylpp_min,ylpp_max,zlpp_min,zlpp_max, & + max_num_drop, max_num_drop_merge, maxnum_cell_drop, max_num_part, max_num_part_cross, & + outputlpp_format,nStepConverion,AspRatioTol, & + WallEffectSettling,output_lpp_evolution,lppbdry_cond,& + TwoWayCouplingFlag,LengthLPP2dp,mfLPP_ref,UnsteadyPartForce,& + SeedParticlesFlag, SeedWithFluidVel, & + AvoidSeedAtBlockBdry,AvoidSeedPartTooClose, & + num_part_seed, time_part_seed, & + xmin_part_seed, ymin_part_seed, zmin_part_seed, dmin_part_seed, & + xmax_part_seed, ymax_part_seed, zmax_part_seed, dmax_part_seed, & + umin_part_seed, vmin_part_seed, wmin_part_seed, & + umax_part_seed, vmax_part_seed, wmax_part_seed, & + maxnum_diff_tag_complet, & + tracked_phase, ConvertMergeDrop,DoOutputLPP,vol_debris + end subroutine +end module diff --git a/tests/fixtures/idempotence/trailing_semicolon.f90 b/tests/fixtures/idempotence/trailing_semicolon.f90 new file mode 100644 index 0000000..c9bfb53 --- /dev/null +++ b/tests/fixtures/idempotence/trailing_semicolon.f90 @@ -0,0 +1,3 @@ +subroutine s + ise=imin; iee=imax; if(coords(1)==0) ise=is; if(coords(1)==nPx-1) iee=ie; +end subroutine diff --git a/tests/fixtures/idempotence/wrapped_declaration.f90 b/tests/fixtures/idempotence/wrapped_declaration.f90 new file mode 100644 index 0000000..fed5078 --- /dev/null +++ b/tests/fixtures/idempotence/wrapped_declaration.f90 @@ -0,0 +1,10 @@ +module m +contains + subroutine s() + character(LEN = name_len) :: dir_name !< + !! Name of the case directory + character(LEN = name_len) :: file_name = 'run_time.inf' !< + !! Name of the run-time information file + x = 1 + end subroutine s +end module m