ffmt 0.4.0__tar.gz → 0.4.2__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 (60) hide show
  1. {ffmt-0.4.0 → ffmt-0.4.2}/.github/workflows/ci.yml +10 -0
  2. {ffmt-0.4.0 → ffmt-0.4.2}/.github/workflows/release.yml +26 -4
  3. {ffmt-0.4.0 → ffmt-0.4.2}/Cargo.lock +15 -15
  4. {ffmt-0.4.0 → ffmt-0.4.2}/Cargo.toml +2 -1
  5. {ffmt-0.4.0 → ffmt-0.4.2}/PKG-INFO +2 -2
  6. {ffmt-0.4.0 → ffmt-0.4.2}/README.md +1 -1
  7. {ffmt-0.4.0 → ffmt-0.4.2}/pyproject.toml +2 -1
  8. {ffmt-0.4.0 → ffmt-0.4.2}/src/classifier.rs +28 -1
  9. {ffmt-0.4.0 → ffmt-0.4.2}/src/cli.rs +42 -24
  10. {ffmt-0.4.0 → ffmt-0.4.2}/src/formatter.rs +117 -50
  11. {ffmt-0.4.0 → ffmt-0.4.2}/src/lsp.rs +119 -12
  12. {ffmt-0.4.0 → ffmt-0.4.2}/src/reader.rs +58 -2
  13. {ffmt-0.4.0 → ffmt-0.4.2}/src/whitespace.rs +75 -38
  14. ffmt-0.4.2/tests/bugfixes.rs +366 -0
  15. ffmt-0.4.2/tests/cli_bugs.rs +128 -0
  16. ffmt-0.4.2/tests/fixtures/member_access_wrap.expected.fpp +29 -0
  17. ffmt-0.4.2/tests/fixtures/member_access_wrap.input.fpp +29 -0
  18. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/trailing_comment_wrap.expected.fpp +2 -2
  19. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/trailing_comment_wrap.input.fpp +2 -2
  20. {ffmt-0.4.0 → ffmt-0.4.2}/tests/integration.rs +17 -0
  21. {ffmt-0.4.0 → ffmt-0.4.2}/tests/reader.rs +56 -1
  22. {ffmt-0.4.0 → ffmt-0.4.2}/.github/workflows/update-readme-version.yml +0 -0
  23. {ffmt-0.4.0 → ffmt-0.4.2}/.gitignore +0 -0
  24. {ffmt-0.4.0 → ffmt-0.4.2}/.markdownlint.json +0 -0
  25. {ffmt-0.4.0 → ffmt-0.4.2}/.pre-commit-hooks.yaml +0 -0
  26. {ffmt-0.4.0 → ffmt-0.4.2}/.readthedocs.yml +0 -0
  27. {ffmt-0.4.0 → ffmt-0.4.2}/LICENSE +0 -0
  28. {ffmt-0.4.0 → ffmt-0.4.2}/action.yml +0 -0
  29. {ffmt-0.4.0 → ffmt-0.4.2}/docs/ci.md +0 -0
  30. {ffmt-0.4.0 → ffmt-0.4.2}/docs/configuration.md +0 -0
  31. {ffmt-0.4.0 → ffmt-0.4.2}/docs/editors.md +0 -0
  32. {ffmt-0.4.0 → ffmt-0.4.2}/docs/getting-started.md +0 -0
  33. {ffmt-0.4.0 → ffmt-0.4.2}/docs/index.md +0 -0
  34. {ffmt-0.4.0 → ffmt-0.4.2}/docs/requirements.txt +0 -0
  35. {ffmt-0.4.0 → ffmt-0.4.2}/mkdocs.yml +0 -0
  36. {ffmt-0.4.0 → ffmt-0.4.2}/src/align.rs +0 -0
  37. {ffmt-0.4.0 → ffmt-0.4.2}/src/case_norm.rs +0 -0
  38. {ffmt-0.4.0 → ffmt-0.4.2}/src/config.rs +0 -0
  39. {ffmt-0.4.0 → ffmt-0.4.2}/src/keyword_norm.rs +0 -0
  40. {ffmt-0.4.0 → ffmt-0.4.2}/src/lib.rs +0 -0
  41. {ffmt-0.4.0 → ffmt-0.4.2}/src/main.rs +0 -0
  42. {ffmt-0.4.0 → ffmt-0.4.2}/src/scope.rs +0 -0
  43. {ffmt-0.4.0 → ffmt-0.4.2}/src/unicode.rs +0 -0
  44. {ffmt-0.4.0 → ffmt-0.4.2}/tests/case_norm.rs +0 -0
  45. {ffmt-0.4.0 → ffmt-0.4.2}/tests/classifier.rs +0 -0
  46. {ffmt-0.4.0 → ffmt-0.4.2}/tests/config_integration.rs +0 -0
  47. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/blank_lines.expected.fpp +0 -0
  48. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/blank_lines.input.fpp +0 -0
  49. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/call_block.expected.fpp +0 -0
  50. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/call_block.input.fpp +0 -0
  51. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/directives.expected.fpp +0 -0
  52. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/directives.input.fpp +0 -0
  53. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/doxygen_spacing.expected.fpp +0 -0
  54. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/doxygen_spacing.input.fpp +0 -0
  55. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/fypp.expected.fpp +0 -0
  56. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/fypp.input.fpp +0 -0
  57. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/simple.expected.fpp +0 -0
  58. {ffmt-0.4.0 → ffmt-0.4.2}/tests/fixtures/simple.input.fpp +0 -0
  59. {ffmt-0.4.0 → ffmt-0.4.2}/tests/scope.rs +0 -0
  60. {ffmt-0.4.0 → ffmt-0.4.2}/tests/whitespace.rs +0 -0
@@ -128,6 +128,15 @@ jobs:
128
128
  matrix:
129
129
  target: [cpu, gpu-omp]
130
130
  steps:
131
+ - name: Free disk space
132
+ uses: jlumbroso/free-disk-space@main
133
+ with:
134
+ tool-cache: false
135
+ android: true
136
+ dotnet: true
137
+ haskell: true
138
+ large-packages: false
139
+
131
140
  - uses: actions/checkout@v4
132
141
 
133
142
  - name: Install dependencies
@@ -206,3 +215,4 @@ jobs:
206
215
  with:
207
216
  name: wheels-${{ matrix.os }}
208
217
  path: dist/
218
+
@@ -9,11 +9,31 @@ permissions:
9
9
 
10
10
  jobs:
11
11
  build-wheels:
12
- name: Build wheels (${{ matrix.os }})
13
- runs-on: ${{ matrix.os }}
12
+ name: Build wheels (${{ matrix.target }})
13
+ runs-on: ${{ matrix.runner }}
14
14
  strategy:
15
15
  matrix:
16
- os: [ubuntu-latest, macos-latest, windows-latest]
16
+ include:
17
+ # Linux x86_64 — manylinux_2_17 for broad HPC compatibility (glibc >= 2.17)
18
+ - runner: ubuntu-latest
19
+ target: x86_64
20
+ manylinux: manylinux_2_17
21
+ # Linux aarch64 — cross-compiled
22
+ - runner: ubuntu-latest
23
+ target: aarch64
24
+ manylinux: manylinux_2_17
25
+ # Linux ppc64le — Frontier/Summit compute nodes
26
+ - runner: ubuntu-latest
27
+ target: ppc64le
28
+ manylinux: manylinux_2_17
29
+ # macOS universal2 — fat binary covering both Intel and Apple Silicon
30
+ - runner: macos-latest
31
+ target: universal2-apple-darwin
32
+ manylinux: ""
33
+ # Windows x86_64
34
+ - runner: windows-latest
35
+ target: x86_64
36
+ manylinux: ""
17
37
  steps:
18
38
  - uses: actions/checkout@v4
19
39
  - uses: actions/setup-python@v5
@@ -23,10 +43,12 @@ jobs:
23
43
  - name: Build wheel
24
44
  uses: PyO3/maturin-action@v1
25
45
  with:
46
+ target: ${{ matrix.target }}
47
+ manylinux: ${{ matrix.manylinux || 'auto' }}
26
48
  args: --release --out dist
27
49
  - uses: actions/upload-artifact@v4
28
50
  with:
29
- name: wheels-${{ matrix.os }}
51
+ name: wheels-${{ matrix.target }}-${{ matrix.runner }}
30
52
  path: dist/
31
53
 
32
54
  build-sdist:
@@ -144,9 +144,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
144
144
 
145
145
  [[package]]
146
146
  name = "either"
147
- version = "1.15.0"
147
+ version = "1.16.0"
148
148
  source = "registry+https://github.com/rust-lang/crates.io-index"
149
- checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
149
+ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
150
150
 
151
151
  [[package]]
152
152
  name = "equivalent"
@@ -156,7 +156,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
156
156
 
157
157
  [[package]]
158
158
  name = "ffmt"
159
- version = "0.4.0"
159
+ version = "0.4.2"
160
160
  dependencies = [
161
161
  "clap",
162
162
  "ignore",
@@ -194,9 +194,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
194
194
 
195
195
  [[package]]
196
196
  name = "ignore"
197
- version = "0.4.25"
197
+ version = "0.4.26"
198
198
  source = "registry+https://github.com/rust-lang/crates.io-index"
199
- checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
199
+ checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d"
200
200
  dependencies = [
201
201
  "crossbeam-deque",
202
202
  "globset",
@@ -232,15 +232,15 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
232
232
 
233
233
  [[package]]
234
234
  name = "log"
235
- version = "0.4.29"
235
+ version = "0.4.32"
236
236
  source = "registry+https://github.com/rust-lang/crates.io-index"
237
- checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
237
+ checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
238
238
 
239
239
  [[package]]
240
240
  name = "memchr"
241
- version = "2.8.0"
241
+ version = "2.8.2"
242
242
  source = "registry+https://github.com/rust-lang/crates.io-index"
243
- checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
243
+ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
244
244
 
245
245
  [[package]]
246
246
  name = "once_cell_polyfill"
@@ -288,9 +288,9 @@ dependencies = [
288
288
 
289
289
  [[package]]
290
290
  name = "regex"
291
- version = "1.12.3"
291
+ version = "1.12.4"
292
292
  source = "registry+https://github.com/rust-lang/crates.io-index"
293
- checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
293
+ checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
294
294
  dependencies = [
295
295
  "aho-corasick",
296
296
  "memchr",
@@ -311,9 +311,9 @@ dependencies = [
311
311
 
312
312
  [[package]]
313
313
  name = "regex-syntax"
314
- version = "0.8.10"
314
+ version = "0.8.11"
315
315
  source = "registry+https://github.com/rust-lang/crates.io-index"
316
- checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
316
+ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
317
317
 
318
318
  [[package]]
319
319
  name = "same-file"
@@ -356,9 +356,9 @@ dependencies = [
356
356
 
357
357
  [[package]]
358
358
  name = "serde_json"
359
- version = "1.0.149"
359
+ version = "1.0.150"
360
360
  source = "registry+https://github.com/rust-lang/crates.io-index"
361
- checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
361
+ checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
362
362
  dependencies = [
363
363
  "itoa",
364
364
  "memchr",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ffmt"
3
- version = "0.4.0"
3
+ version = "0.4.2"
4
4
  edition = "2021"
5
5
  description = "A fast, configurable Fortran formatter with Fypp, Doxygen, and OpenACC/OpenMP support"
6
6
  license = "MIT"
@@ -17,3 +17,4 @@ ignore = "0.4"
17
17
  toml = "0.8"
18
18
  serde = { version = "1", features = ["derive"] }
19
19
  serde_json = "1"
20
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ffmt
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Science/Research
@@ -88,7 +88,7 @@ multdiv = false
88
88
  # pre-commit
89
89
  repos:
90
90
  - repo: https://github.com/sbryngelson/ffmt
91
- rev: v0.4.0
91
+ rev: v0.4.2
92
92
  hooks:
93
93
  - id: ffmt
94
94
  ```
@@ -71,7 +71,7 @@ multdiv = false
71
71
  # pre-commit
72
72
  repos:
73
73
  - repo: https://github.com/sbryngelson/ffmt
74
- rev: v0.4.0
74
+ rev: v0.4.2
75
75
  hooks:
76
76
  - id: ffmt
77
77
  ```
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "ffmt"
7
- version = "0.4.0"
7
+ version = "0.4.2"
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"
@@ -23,3 +23,4 @@ classifiers = [
23
23
  [tool.maturin]
24
24
  # Binary-only distribution — no Python bindings, just the ffmt CLI
25
25
  bindings = "bin"
26
+
@@ -100,11 +100,31 @@ fn strip_trailing_comment(s: &str) -> String {
100
100
 
101
101
  /// Find the matching closing paren for the paren at position `start`.
102
102
  /// Returns the index of the closing paren, or None if not found.
103
+ /// Parens inside string literals are ignored (e.g., `if (ch == '(') then`).
103
104
  fn find_matching_paren(s: &str, start: usize) -> Option<usize> {
104
105
  let bytes = s.as_bytes();
105
106
  let mut depth = 0i32;
106
- for (i, &byte) in bytes.iter().enumerate().skip(start) {
107
+ let mut in_string = false;
108
+ let mut quote = b' ';
109
+ let mut i = start;
110
+ while i < bytes.len() {
111
+ let byte = bytes[i];
112
+ if in_string {
113
+ if byte == quote {
114
+ if i + 1 < bytes.len() && bytes[i + 1] == quote {
115
+ i += 2; // escaped quote — stay in string
116
+ continue;
117
+ }
118
+ in_string = false;
119
+ }
120
+ i += 1;
121
+ continue;
122
+ }
107
123
  match byte {
124
+ b'\'' | b'"' => {
125
+ in_string = true;
126
+ quote = byte;
127
+ }
108
128
  b'(' => depth += 1,
109
129
  b')' => {
110
130
  depth -= 1;
@@ -114,6 +134,7 @@ fn find_matching_paren(s: &str, start: usize) -> Option<usize> {
114
134
  }
115
135
  _ => {}
116
136
  }
137
+ i += 1;
117
138
  }
118
139
  None
119
140
  }
@@ -178,6 +199,12 @@ fn classify_fortran(trimmed: &str) -> LineKind {
178
199
  }
179
200
  };
180
201
 
202
+ // Strip any trailing comment so checks like `lower == "contains"` and the
203
+ // "what follows the closing paren" tests are not confused by comment text
204
+ // (e.g., `contains ! public api`, `where (mask > 0) ! note`).
205
+ let stripped = strip_trailing_comment(line);
206
+ let line = stripped.as_str();
207
+
181
208
  let lower = line.to_ascii_lowercase();
182
209
 
183
210
  // --- Block closers (check first since "end" prefix is distinctive) ---
@@ -6,7 +6,6 @@ use std::fs;
6
6
  use std::io::{self, Read, Write};
7
7
  use std::path::{Path, PathBuf};
8
8
  use std::process;
9
- use std::sync::atomic::{AtomicBool, Ordering};
10
9
 
11
10
  /// Default cache directory (relative to cwd).
12
11
  const CACHE_DIR: &str = ".ffmt_cache";
@@ -201,7 +200,6 @@ pub fn run() {
201
200
  };
202
201
 
203
202
  let range = parse_range(&args.range);
204
- let any_changed = AtomicBool::new(false);
205
203
 
206
204
  let opts = ProcessOptions {
207
205
  check: args.check,
@@ -212,25 +210,37 @@ pub fn run() {
212
210
  verbose,
213
211
  };
214
212
 
215
- files_to_process.par_iter().for_each(|path| {
216
- let changed = process_file(path, &opts, &config);
217
- if changed {
218
- any_changed.store(true, Ordering::Relaxed);
219
- }
220
- });
221
-
222
- // Update cache for all files (including ones we skipped — they're still valid)
213
+ let outcomes: Vec<(&PathBuf, Outcome)> = files_to_process
214
+ .par_iter()
215
+ .map(|path| (*path, process_file(path, &opts, &config)))
216
+ .collect();
217
+ let any_changed = outcomes.iter().any(|(_, o)| *o == Outcome::Changed);
218
+ let any_failed = outcomes.iter().any(|(_, o)| *o == Outcome::Failed);
219
+ let failed: std::collections::HashSet<&PathBuf> = outcomes
220
+ .iter()
221
+ .filter(|(_, o)| *o == Outcome::Failed)
222
+ .map(|(p, _)| *p)
223
+ .collect();
224
+
225
+ // Update cache for successfully processed files (including ones we skipped
226
+ // via the cache — they're still valid). Files that failed must NOT be
227
+ // cached, or the failure would be silently skipped on the next run.
223
228
  if let Some(ref mut cache) = cache {
224
229
  // Only update cache when formatting (not in check/diff mode)
225
230
  if !args.check && !args.diff {
226
231
  for f in &files {
227
- cache.update(f);
232
+ if !failed.contains(f) {
233
+ cache.update(f);
234
+ }
228
235
  }
229
236
  cache.save(cache_dir);
230
237
  }
231
238
  }
232
239
 
233
- if args.check && any_changed.load(Ordering::Relaxed) {
240
+ if any_failed {
241
+ process::exit(2);
242
+ }
243
+ if args.check && any_changed {
234
244
  process::exit(1);
235
245
  }
236
246
  }
@@ -276,7 +286,10 @@ fn run_stdin(args: &Args, color: bool, quiet: bool, config: &crate::config::Conf
276
286
  if args.diff {
277
287
  print_diff(Path::new(filepath), &source, &formatted, color);
278
288
  }
279
- process::exit(1);
289
+ // --diff alone is informational; only --check signals failure
290
+ if args.check {
291
+ process::exit(1);
292
+ }
280
293
  }
281
294
  } else {
282
295
  io::stdout()
@@ -332,7 +345,7 @@ fn discover_files_with_config(
332
345
 
333
346
  for entry in builder.build().flatten() {
334
347
  let entry_path = entry.path();
335
- if entry_path.is_file() && is_fortran_file(entry_path) {
348
+ if entry_path.is_file() && is_fortran_file_ext(entry_path, extensions) {
336
349
  files.push(entry_path.to_path_buf());
337
350
  }
338
351
  }
@@ -343,10 +356,6 @@ fn discover_files_with_config(
343
356
  files
344
357
  }
345
358
 
346
- fn is_fortran_file(path: &Path) -> bool {
347
- is_fortran_file_ext(path, &crate::config::FilesConfig::default().extensions)
348
- }
349
-
350
359
  fn is_fortran_file_ext(path: &Path, extensions: &[String]) -> bool {
351
360
  path.extension()
352
361
  .and_then(|e| e.to_str())
@@ -362,16 +371,24 @@ struct ProcessOptions {
362
371
  verbose: bool,
363
372
  }
364
373
 
365
- fn process_file(path: &Path, opts: &ProcessOptions, config: &crate::config::Config) -> bool {
374
+ /// Result of processing one file.
375
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
376
+ enum Outcome {
377
+ Unchanged,
378
+ Changed,
379
+ Failed,
380
+ }
381
+
382
+ fn process_file(path: &Path, opts: &ProcessOptions, config: &crate::config::Config) -> Outcome {
366
383
  let source = match fs::read_to_string(path) {
367
384
  Ok(s) => s,
368
385
  Err(e) if e.kind() == io::ErrorKind::InvalidData => {
369
386
  // Binary or invalid UTF-8: not a text Fortran file, skip silently
370
- return false;
387
+ return Outcome::Unchanged;
371
388
  }
372
389
  Err(e) => {
373
390
  eprintln!("ffmt: cannot read {}: {e}", path.display());
374
- return false;
391
+ return Outcome::Failed;
375
392
  }
376
393
  };
377
394
 
@@ -382,7 +399,7 @@ fn process_file(path: &Path, opts: &ProcessOptions, config: &crate::config::Conf
382
399
  let formatted = crate::formatter::format_with_config(&source, config, opts.range);
383
400
 
384
401
  if formatted == source {
385
- return false;
402
+ return Outcome::Unchanged;
386
403
  }
387
404
 
388
405
  if opts.check || opts.diff {
@@ -392,14 +409,15 @@ fn process_file(path: &Path, opts: &ProcessOptions, config: &crate::config::Conf
392
409
  if opts.diff {
393
410
  print_diff(path, &source, &formatted, opts.color);
394
411
  }
395
- return true;
412
+ return Outcome::Changed;
396
413
  }
397
414
 
398
415
  if let Err(e) = fs::write(path, &formatted) {
399
416
  eprintln!("ffmt: cannot write {}: {e}", path.display());
417
+ return Outcome::Failed;
400
418
  }
401
419
 
402
- true
420
+ Outcome::Changed
403
421
  }
404
422
 
405
423
  fn print_diff(path: &Path, original: &str, formatted: &str, color: bool) {