ffmt 0.2.6__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {ffmt-0.2.6 → ffmt-0.2.8}/Cargo.lock +1 -1
  2. {ffmt-0.2.6 → ffmt-0.2.8}/Cargo.toml +1 -1
  3. {ffmt-0.2.6 → ffmt-0.2.8}/PKG-INFO +1 -1
  4. {ffmt-0.2.6 → ffmt-0.2.8}/pyproject.toml +1 -1
  5. {ffmt-0.2.6 → ffmt-0.2.8}/src/align.rs +5 -5
  6. {ffmt-0.2.6 → ffmt-0.2.8}/src/formatter.rs +150 -10
  7. {ffmt-0.2.6 → ffmt-0.2.8}/.github/workflows/ci.yml +0 -0
  8. {ffmt-0.2.6 → ffmt-0.2.8}/.github/workflows/release.yml +0 -0
  9. {ffmt-0.2.6 → ffmt-0.2.8}/.github/workflows/update-readme-version.yml +0 -0
  10. {ffmt-0.2.6 → ffmt-0.2.8}/.gitignore +0 -0
  11. {ffmt-0.2.6 → ffmt-0.2.8}/.pre-commit-hooks.yaml +0 -0
  12. {ffmt-0.2.6 → ffmt-0.2.8}/LICENSE +0 -0
  13. {ffmt-0.2.6 → ffmt-0.2.8}/README.md +0 -0
  14. {ffmt-0.2.6 → ffmt-0.2.8}/action.yml +0 -0
  15. {ffmt-0.2.6 → ffmt-0.2.8}/src/case_norm.rs +0 -0
  16. {ffmt-0.2.6 → ffmt-0.2.8}/src/classifier.rs +0 -0
  17. {ffmt-0.2.6 → ffmt-0.2.8}/src/cli.rs +0 -0
  18. {ffmt-0.2.6 → ffmt-0.2.8}/src/config.rs +0 -0
  19. {ffmt-0.2.6 → ffmt-0.2.8}/src/keyword_norm.rs +0 -0
  20. {ffmt-0.2.6 → ffmt-0.2.8}/src/lib.rs +0 -0
  21. {ffmt-0.2.6 → ffmt-0.2.8}/src/lsp.rs +0 -0
  22. {ffmt-0.2.6 → ffmt-0.2.8}/src/main.rs +0 -0
  23. {ffmt-0.2.6 → ffmt-0.2.8}/src/reader.rs +0 -0
  24. {ffmt-0.2.6 → ffmt-0.2.8}/src/scope.rs +0 -0
  25. {ffmt-0.2.6 → ffmt-0.2.8}/src/unicode.rs +0 -0
  26. {ffmt-0.2.6 → ffmt-0.2.8}/src/whitespace.rs +0 -0
  27. {ffmt-0.2.6 → ffmt-0.2.8}/tests/case_norm.rs +0 -0
  28. {ffmt-0.2.6 → ffmt-0.2.8}/tests/classifier.rs +0 -0
  29. {ffmt-0.2.6 → ffmt-0.2.8}/tests/config_integration.rs +0 -0
  30. {ffmt-0.2.6 → ffmt-0.2.8}/tests/fixtures/blank_lines.expected.fpp +0 -0
  31. {ffmt-0.2.6 → ffmt-0.2.8}/tests/fixtures/blank_lines.input.fpp +0 -0
  32. {ffmt-0.2.6 → ffmt-0.2.8}/tests/fixtures/call_block.expected.fpp +0 -0
  33. {ffmt-0.2.6 → ffmt-0.2.8}/tests/fixtures/call_block.input.fpp +0 -0
  34. {ffmt-0.2.6 → ffmt-0.2.8}/tests/fixtures/directives.expected.fpp +0 -0
  35. {ffmt-0.2.6 → ffmt-0.2.8}/tests/fixtures/directives.input.fpp +0 -0
  36. {ffmt-0.2.6 → ffmt-0.2.8}/tests/fixtures/doxygen_spacing.expected.fpp +0 -0
  37. {ffmt-0.2.6 → ffmt-0.2.8}/tests/fixtures/doxygen_spacing.input.fpp +0 -0
  38. {ffmt-0.2.6 → ffmt-0.2.8}/tests/fixtures/fypp.expected.fpp +0 -0
  39. {ffmt-0.2.6 → ffmt-0.2.8}/tests/fixtures/fypp.input.fpp +0 -0
  40. {ffmt-0.2.6 → ffmt-0.2.8}/tests/fixtures/simple.expected.fpp +0 -0
  41. {ffmt-0.2.6 → ffmt-0.2.8}/tests/fixtures/simple.input.fpp +0 -0
  42. {ffmt-0.2.6 → ffmt-0.2.8}/tests/integration.rs +0 -0
  43. {ffmt-0.2.6 → ffmt-0.2.8}/tests/reader.rs +0 -0
  44. {ffmt-0.2.6 → ffmt-0.2.8}/tests/scope.rs +0 -0
  45. {ffmt-0.2.6 → ffmt-0.2.8}/tests/whitespace.rs +0 -0
@@ -156,7 +156,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
156
156
 
157
157
  [[package]]
158
158
  name = "ffmt"
159
- version = "0.2.6"
159
+ version = "0.2.8"
160
160
  dependencies = [
161
161
  "clap",
162
162
  "ignore",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ffmt"
3
- version = "0.2.6"
3
+ version = "0.2.8"
4
4
  edition = "2021"
5
5
  description = "A fast, configurable Fortran formatter with Fypp, Doxygen, and OpenACC/OpenMP support"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ffmt
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Science/Research
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "ffmt"
7
- version = "0.2.6"
7
+ version = "0.2.8"
8
8
  description = "A fast, configurable Fortran formatter with Fypp, Doxygen, and OpenACC/OpenMP support"
9
9
  license = { text = "MIT" }
10
10
  requires-python = ">=3.8"
@@ -167,7 +167,7 @@ fn align_inline_comments(lines: &mut [String], line_length: usize) {
167
167
  if let Some(pos) = find_inline_doxygen(line) {
168
168
  if !has_doxygen_text(line, pos) { continue; }
169
169
  let comment = &line[pos..];
170
- let aligned_len = max_code_len + 1 + comment.len();
170
+ let aligned_len = max_code_len + 2 + comment.len();
171
171
  if aligned_len > line_length {
172
172
  would_overflow = true;
173
173
  break;
@@ -176,14 +176,14 @@ fn align_inline_comments(lines: &mut [String], line_length: usize) {
176
176
  }
177
177
 
178
178
  if would_overflow {
179
- // Fall back to minimal spacing: just ensure 1 space before !<
179
+ // Fall back to minimal spacing: ensure 2 spaces before !<
180
180
  for line in lines.iter_mut() {
181
181
  if let Some(pos) = find_inline_doxygen(line) {
182
182
  if !has_doxygen_text(line, pos) { continue; }
183
183
  let before = &line[..pos];
184
184
  let comment = &line[pos..];
185
185
  let trimmed_before = before.trim_end();
186
- *line = format!("{} {}", trimmed_before, comment);
186
+ *line = format!("{} {}", trimmed_before, comment);
187
187
  }
188
188
  }
189
189
  return;
@@ -196,8 +196,8 @@ fn align_inline_comments(lines: &mut [String], line_length: usize) {
196
196
  let before = &line[..pos];
197
197
  let comment = &line[pos..];
198
198
  let trimmed_before = before.trim_end();
199
- let padding = max_code_len - trimmed_before.len();
200
- *line = format!("{}{} {}", trimmed_before, " ".repeat(padding), comment);
199
+ let padding = max_code_len + 2 - trimmed_before.len();
200
+ *line = format!("{}{}{}", trimmed_before, " ".repeat(padding), comment);
201
201
  }
202
202
  }
203
203
  }
@@ -435,7 +435,8 @@ pub fn format_with_config(
435
435
  if config.rewrap_code {
436
436
  let mut final_lines: Vec<String> = Vec::new();
437
437
  for line in &output_lines {
438
- if line.len() > config.line_length && find_inline_doxygen_in_line(line).is_none() {
438
+ if line.len() > config.line_length && find_inline_doxygen_in_line(line).is_none()
439
+ && !line.trim_end().ends_with('&') {
439
440
  final_lines.extend(rewrap_line(line, config.line_length, config.indent_width));
440
441
  } else {
441
442
  final_lines.push(line.clone());
@@ -447,6 +448,13 @@ pub fn format_with_config(
447
448
  // Align :: in consecutive declaration lines
448
449
  if config.align_declarations { output_lines = crate::align::align_declarations(&output_lines, config.compact_declarations, config.align_comments, config.line_length); }
449
450
 
451
+ // Ensure at least 2 spaces before inline comments (Fortitude S102)
452
+ // Runs after alignment so !< comments are already positioned
453
+ output_lines = ensure_two_spaces_before_inline_comment(&output_lines);
454
+
455
+ // Strip trailing semicolons (Fortitude S081)
456
+ output_lines = strip_trailing_semicolons(&output_lines);
457
+
450
458
  let mut result = output_lines.join("\n");
451
459
  if !result.is_empty() && !result.ends_with('\n') {
452
460
  result.push('\n');
@@ -1200,6 +1208,108 @@ fn is_declaration_line(line: &str) -> bool {
1200
1208
  false
1201
1209
  }
1202
1210
 
1211
+ /// Ensure at least 2 spaces before inline `!` comments on code lines.
1212
+ /// This satisfies Fortitude rule S102. Only applies to lines that have both
1213
+ /// code and a trailing comment (not standalone comment lines, not `!$` directives,
1214
+ /// not `!>` or `!<` Doxygen, not Fypp lines).
1215
+ fn ensure_two_spaces_before_inline_comment(lines: &[String]) -> Vec<String> {
1216
+ lines.iter().map(|line| {
1217
+ let trimmed = line.trim_start();
1218
+ // Skip blank lines, standalone comments, preprocessor, Fypp
1219
+ if trimmed.is_empty() || trimmed.starts_with('!') || trimmed.starts_with('#')
1220
+ || trimmed.starts_with("$:") || trimmed.starts_with("@:") {
1221
+ return line.clone();
1222
+ }
1223
+ // Find the inline comment position (! outside strings)
1224
+ let bytes = line.as_bytes();
1225
+ let mut in_string = false;
1226
+ let mut delim = b' ';
1227
+ for i in 0..bytes.len() {
1228
+ if in_string {
1229
+ if bytes[i] == delim {
1230
+ // Check for doubled quote escape
1231
+ if i + 1 < bytes.len() && bytes[i + 1] == delim {
1232
+ continue; // skip
1233
+ }
1234
+ in_string = false;
1235
+ }
1236
+ } else if bytes[i] == b'\'' || bytes[i] == b'"' {
1237
+ in_string = true;
1238
+ delim = bytes[i];
1239
+ } else if bytes[i] == b'!' {
1240
+ // Found inline comment. Check spacing before it.
1241
+ if i == 0 { return line.clone(); } // standalone comment
1242
+ // Skip directives (!$acc, !$omp), standalone Doxygen (!>), and !! continuations
1243
+ let rest = &line[i..];
1244
+ if rest.starts_with("!$") || rest.starts_with("!>") || rest.starts_with("!!") {
1245
+ return line.clone();
1246
+ }
1247
+ // Count spaces before the `!`
1248
+ let mut spaces = 0;
1249
+ let mut j = i;
1250
+ while j > 0 && bytes[j - 1] == b' ' {
1251
+ spaces += 1;
1252
+ j -= 1;
1253
+ }
1254
+ if j == 0 { return line.clone(); } // all spaces before ! = standalone
1255
+ if spaces >= 2 { return line.clone(); } // already ok
1256
+ // Add spaces to reach 2, but only if it won't exceed line_length
1257
+ let code_part = &line[..i].trim_end();
1258
+ let comment_part = &line[i..];
1259
+ return format!("{} {}", code_part, comment_part);
1260
+ }
1261
+ }
1262
+ line.clone()
1263
+ }).collect()
1264
+ }
1265
+
1266
+ /// Strip trailing semicolons from lines (Fortitude S081).
1267
+ /// Removes `;` at the end of a line (before any trailing comment), but only if
1268
+ /// it's truly trailing — not between statements on the same line.
1269
+ fn strip_trailing_semicolons(lines: &[String]) -> Vec<String> {
1270
+ lines.iter().map(|line| {
1271
+ let trimmed = line.trim_start();
1272
+ // Skip blank, comment-only, preprocessor, Fypp lines
1273
+ if trimmed.is_empty() || trimmed.starts_with('!') || trimmed.starts_with('#')
1274
+ || trimmed.starts_with("$:") || trimmed.starts_with("@:") {
1275
+ return line.clone();
1276
+ }
1277
+ // Find the code portion (before any inline comment)
1278
+ let bytes = line.as_bytes();
1279
+ let mut in_string = false;
1280
+ let mut delim = b' ';
1281
+ let mut comment_start = bytes.len();
1282
+ for i in 0..bytes.len() {
1283
+ if in_string {
1284
+ if bytes[i] == delim {
1285
+ if i + 1 < bytes.len() && bytes[i + 1] == delim {
1286
+ continue;
1287
+ }
1288
+ in_string = false;
1289
+ }
1290
+ } else if bytes[i] == b'\'' || bytes[i] == b'"' {
1291
+ in_string = true;
1292
+ delim = bytes[i];
1293
+ } else if bytes[i] == b'!' {
1294
+ comment_start = i;
1295
+ break;
1296
+ }
1297
+ }
1298
+ let code = &line[..comment_start];
1299
+ let comment = &line[comment_start..];
1300
+ let code_trimmed = code.trim_end();
1301
+ if code_trimmed.ends_with(';') {
1302
+ let stripped = code_trimmed.strip_suffix(';').unwrap().trim_end();
1303
+ if comment.is_empty() {
1304
+ return format!("{}{}", &line[..line.len() - line.trim_start().len()], stripped.trim_start());
1305
+ } else {
1306
+ return format!("{} {}", stripped, comment.trim_start());
1307
+ }
1308
+ }
1309
+ line.clone()
1310
+ }).collect()
1311
+ }
1312
+
1203
1313
  /// Ensure a blank line between the last declaration and the first executable statement
1204
1314
  /// within procedure bodies. Only acts when there is no blank line already.
1205
1315
  fn separate_declarations_from_code(lines: &[String]) -> Vec<String> {
@@ -1442,10 +1552,27 @@ fn rewrap_line(line: &str, max_length: usize, indent_width: usize) -> Vec<String
1442
1552
  return vec![line.to_string()];
1443
1553
  }
1444
1554
 
1445
- // Align continuation lines with opening paren if present,
1446
- // otherwise use flat indent (one indent_width deeper).
1447
- let paren_col = content.find('(').map(|p| indent + p + 1);
1448
- let cont_indent = paren_col.unwrap_or(indent + indent_width);
1555
+ // Align continuation with opening paren (string-aware), capped to avoid extreme indentation
1556
+ let flat_indent = indent + indent_width;
1557
+ let paren_col = {
1558
+ let mut in_str = false;
1559
+ let mut qch = b' ';
1560
+ let mut found = None;
1561
+ let cb = content.as_bytes();
1562
+ for i in 0..cb.len() {
1563
+ if in_str {
1564
+ if cb[i] == qch { if i + 1 < cb.len() && cb[i + 1] == qch { continue; } in_str = false; }
1565
+ continue;
1566
+ }
1567
+ if cb[i] == b'\'' || cb[i] == b'"' { in_str = true; qch = cb[i]; continue; }
1568
+ if cb[i] == b'(' { found = Some(indent + i + 1); break; }
1569
+ }
1570
+ found
1571
+ };
1572
+ let cont_indent = match paren_col {
1573
+ Some(col) if col <= indent + 40 => col,
1574
+ _ => flat_indent,
1575
+ };
1449
1576
  let cont_prefix = " ".repeat(cont_indent);
1450
1577
 
1451
1578
  // Available width: max_length minus indent minus " &" suffix
@@ -1498,13 +1625,26 @@ fn rewrap_line(line: &str, max_length: usize, indent_width: usize) -> Vec<String
1498
1625
  } else if last_other > 0 {
1499
1626
  last_other
1500
1627
  } else {
1501
- // No break point foundemit as-is (over-length)
1502
- if is_first {
1503
- result.push(format!("{}{}", " ".repeat(indent), remaining));
1628
+ // No break within limitfind first comma AFTER limit (long string literal)
1629
+ let mut first_comma_after = 0usize;
1630
+ for &(bp, kind) in &breaks {
1631
+ if bp <= pos { continue; }
1632
+ if matches!(kind, BreakKind::Comma) {
1633
+ first_comma_after = bp - pos;
1634
+ break;
1635
+ }
1636
+ }
1637
+ if first_comma_after > 0 {
1638
+ first_comma_after
1504
1639
  } else {
1505
- result.push(format!("{}& {}", cont_prefix, remaining));
1640
+ // No break point at all — emit as-is
1641
+ if is_first {
1642
+ result.push(format!("{}{}", " ".repeat(indent), remaining));
1643
+ } else {
1644
+ result.push(format!("{}& {}", cont_prefix, remaining));
1645
+ }
1646
+ break;
1506
1647
  }
1507
- break;
1508
1648
  };
1509
1649
 
1510
1650
  let chunk = content[pos..pos + best_break].trim_end();
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes