ffmt 0.4.2__tar.gz → 0.4.4__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 (66) hide show
  1. {ffmt-0.4.2 → ffmt-0.4.4}/Cargo.lock +26 -26
  2. {ffmt-0.4.2 → ffmt-0.4.4}/Cargo.toml +1 -1
  3. {ffmt-0.4.2 → ffmt-0.4.4}/PKG-INFO +2 -2
  4. {ffmt-0.4.2 → ffmt-0.4.4}/README.md +1 -1
  5. {ffmt-0.4.2 → ffmt-0.4.4}/pyproject.toml +1 -1
  6. {ffmt-0.4.2 → ffmt-0.4.4}/src/case_norm.rs +32 -4
  7. {ffmt-0.4.2 → ffmt-0.4.4}/src/classifier.rs +129 -11
  8. {ffmt-0.4.2 → ffmt-0.4.4}/src/cli.rs +144 -39
  9. {ffmt-0.4.2 → ffmt-0.4.4}/src/config.rs +17 -11
  10. {ffmt-0.4.2 → ffmt-0.4.4}/src/formatter.rs +314 -146
  11. {ffmt-0.4.2 → ffmt-0.4.4}/src/keyword_norm.rs +31 -4
  12. {ffmt-0.4.2 → ffmt-0.4.4}/src/lsp.rs +14 -2
  13. ffmt-0.4.4/src/reader.rs +541 -0
  14. {ffmt-0.4.2 → ffmt-0.4.4}/src/whitespace.rs +81 -19
  15. ffmt-0.4.4/tests/bugfixes.rs +788 -0
  16. ffmt-0.4.4/tests/bughunt_classifier.rs +357 -0
  17. ffmt-0.4.4/tests/bughunt_cli.rs +470 -0
  18. ffmt-0.4.4/tests/bughunt_norm.rs +185 -0
  19. {ffmt-0.4.2 → ffmt-0.4.4}/tests/classifier.rs +55 -0
  20. ffmt-0.4.4/tests/reader.rs +291 -0
  21. ffmt-0.4.2/src/reader.rs +0 -391
  22. ffmt-0.4.2/tests/bugfixes.rs +0 -366
  23. ffmt-0.4.2/tests/reader.rs +0 -139
  24. {ffmt-0.4.2 → ffmt-0.4.4}/.github/workflows/ci.yml +0 -0
  25. {ffmt-0.4.2 → ffmt-0.4.4}/.github/workflows/release.yml +0 -0
  26. {ffmt-0.4.2 → ffmt-0.4.4}/.github/workflows/update-readme-version.yml +0 -0
  27. {ffmt-0.4.2 → ffmt-0.4.4}/.gitignore +0 -0
  28. {ffmt-0.4.2 → ffmt-0.4.4}/.markdownlint.json +0 -0
  29. {ffmt-0.4.2 → ffmt-0.4.4}/.pre-commit-hooks.yaml +0 -0
  30. {ffmt-0.4.2 → ffmt-0.4.4}/.readthedocs.yml +0 -0
  31. {ffmt-0.4.2 → ffmt-0.4.4}/LICENSE +0 -0
  32. {ffmt-0.4.2 → ffmt-0.4.4}/action.yml +0 -0
  33. {ffmt-0.4.2 → ffmt-0.4.4}/docs/ci.md +0 -0
  34. {ffmt-0.4.2 → ffmt-0.4.4}/docs/configuration.md +0 -0
  35. {ffmt-0.4.2 → ffmt-0.4.4}/docs/editors.md +0 -0
  36. {ffmt-0.4.2 → ffmt-0.4.4}/docs/getting-started.md +0 -0
  37. {ffmt-0.4.2 → ffmt-0.4.4}/docs/index.md +0 -0
  38. {ffmt-0.4.2 → ffmt-0.4.4}/docs/requirements.txt +0 -0
  39. {ffmt-0.4.2 → ffmt-0.4.4}/mkdocs.yml +0 -0
  40. {ffmt-0.4.2 → ffmt-0.4.4}/src/align.rs +0 -0
  41. {ffmt-0.4.2 → ffmt-0.4.4}/src/lib.rs +0 -0
  42. {ffmt-0.4.2 → ffmt-0.4.4}/src/main.rs +0 -0
  43. {ffmt-0.4.2 → ffmt-0.4.4}/src/scope.rs +0 -0
  44. {ffmt-0.4.2 → ffmt-0.4.4}/src/unicode.rs +0 -0
  45. {ffmt-0.4.2 → ffmt-0.4.4}/tests/case_norm.rs +0 -0
  46. {ffmt-0.4.2 → ffmt-0.4.4}/tests/cli_bugs.rs +0 -0
  47. {ffmt-0.4.2 → ffmt-0.4.4}/tests/config_integration.rs +0 -0
  48. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/blank_lines.expected.fpp +0 -0
  49. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/blank_lines.input.fpp +0 -0
  50. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/call_block.expected.fpp +0 -0
  51. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/call_block.input.fpp +0 -0
  52. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/directives.expected.fpp +0 -0
  53. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/directives.input.fpp +0 -0
  54. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/doxygen_spacing.expected.fpp +0 -0
  55. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/doxygen_spacing.input.fpp +0 -0
  56. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/fypp.expected.fpp +0 -0
  57. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/fypp.input.fpp +0 -0
  58. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/member_access_wrap.expected.fpp +0 -0
  59. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/member_access_wrap.input.fpp +0 -0
  60. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/simple.expected.fpp +0 -0
  61. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/simple.input.fpp +0 -0
  62. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/trailing_comment_wrap.expected.fpp +0 -0
  63. {ffmt-0.4.2 → ffmt-0.4.4}/tests/fixtures/trailing_comment_wrap.input.fpp +0 -0
  64. {ffmt-0.4.2 → ffmt-0.4.4}/tests/integration.rs +0 -0
  65. {ffmt-0.4.2 → ffmt-0.4.4}/tests/scope.rs +0 -0
  66. {ffmt-0.4.2 → ffmt-0.4.4}/tests/whitespace.rs +0 -0
@@ -63,12 +63,12 @@ dependencies = [
63
63
 
64
64
  [[package]]
65
65
  name = "bstr"
66
- version = "1.12.1"
66
+ version = "1.12.3"
67
67
  source = "registry+https://github.com/rust-lang/crates.io-index"
68
- checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
68
+ checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79"
69
69
  dependencies = [
70
70
  "memchr",
71
- "serde",
71
+ "serde_core",
72
72
  ]
73
73
 
74
74
  [[package]]
@@ -119,9 +119,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
119
119
 
120
120
  [[package]]
121
121
  name = "crossbeam-deque"
122
- version = "0.8.6"
122
+ version = "0.8.7"
123
123
  source = "registry+https://github.com/rust-lang/crates.io-index"
124
- checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
124
+ checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
125
125
  dependencies = [
126
126
  "crossbeam-epoch",
127
127
  "crossbeam-utils",
@@ -129,18 +129,18 @@ dependencies = [
129
129
 
130
130
  [[package]]
131
131
  name = "crossbeam-epoch"
132
- version = "0.9.18"
132
+ version = "0.9.20"
133
133
  source = "registry+https://github.com/rust-lang/crates.io-index"
134
- checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
134
+ checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
135
135
  dependencies = [
136
136
  "crossbeam-utils",
137
137
  ]
138
138
 
139
139
  [[package]]
140
140
  name = "crossbeam-utils"
141
- version = "0.8.21"
141
+ version = "0.8.22"
142
142
  source = "registry+https://github.com/rust-lang/crates.io-index"
143
- checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
143
+ checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
144
144
 
145
145
  [[package]]
146
146
  name = "either"
@@ -156,7 +156,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
156
156
 
157
157
  [[package]]
158
158
  name = "ffmt"
159
- version = "0.4.2"
159
+ version = "0.4.4"
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.26"
197
+ version = "0.4.28"
198
198
  source = "registry+https://github.com/rust-lang/crates.io-index"
199
- checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d"
199
+ checksum = "2adf14691c72bcfc1058740436a35bdd3ae9c07d1a941ef00b749e9ea16aefa7"
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.32"
235
+ version = "0.4.33"
236
236
  source = "registry+https://github.com/rust-lang/crates.io-index"
237
- checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
237
+ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
238
238
 
239
239
  [[package]]
240
240
  name = "memchr"
241
- version = "2.8.2"
241
+ version = "2.8.3"
242
242
  source = "registry+https://github.com/rust-lang/crates.io-index"
243
- checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
243
+ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
244
244
 
245
245
  [[package]]
246
246
  name = "once_cell_polyfill"
@@ -259,9 +259,9 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "quote"
262
- version = "1.0.45"
262
+ version = "1.0.46"
263
263
  source = "registry+https://github.com/rust-lang/crates.io-index"
264
- checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
264
+ checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
265
265
  dependencies = [
266
266
  "proc-macro2",
267
267
  ]
@@ -288,9 +288,9 @@ dependencies = [
288
288
 
289
289
  [[package]]
290
290
  name = "regex"
291
- version = "1.12.4"
291
+ version = "1.13.0"
292
292
  source = "registry+https://github.com/rust-lang/crates.io-index"
293
- checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
293
+ checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2"
294
294
  dependencies = [
295
295
  "aho-corasick",
296
296
  "memchr",
@@ -300,9 +300,9 @@ dependencies = [
300
300
 
301
301
  [[package]]
302
302
  name = "regex-automata"
303
- version = "0.4.14"
303
+ version = "0.4.15"
304
304
  source = "registry+https://github.com/rust-lang/crates.io-index"
305
- checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
305
+ checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db"
306
306
  dependencies = [
307
307
  "aho-corasick",
308
308
  "memchr",
@@ -384,9 +384,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
384
384
 
385
385
  [[package]]
386
386
  name = "syn"
387
- version = "2.0.117"
387
+ version = "2.0.118"
388
388
  source = "registry+https://github.com/rust-lang/crates.io-index"
389
- checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
389
+ checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
390
390
  dependencies = [
391
391
  "proc-macro2",
392
392
  "quote",
@@ -491,6 +491,6 @@ dependencies = [
491
491
 
492
492
  [[package]]
493
493
  name = "zmij"
494
- version = "1.0.21"
494
+ version = "1.0.23"
495
495
  source = "registry+https://github.com/rust-lang/crates.io-index"
496
- checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
496
+ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ffmt"
3
- version = "0.4.2"
3
+ version = "0.4.4"
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.4.2
3
+ Version: 0.4.4
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.2
91
+ rev: v0.4.3
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.2
74
+ rev: v0.4.3
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.2"
7
+ version = "0.4.4"
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"
@@ -8,6 +8,15 @@
8
8
  /// lowercased in non-opaque regions.
9
9
  /// - All other text (identifiers, numbers, punctuation) is preserved.
10
10
  pub fn normalize_case(line: &str) -> String {
11
+ normalize_case_with(line, false)
12
+ }
13
+
14
+ /// Normalize case of Fortran keywords to UPPERCASE (keyword-case = "upper").
15
+ pub fn normalize_case_upper(line: &str) -> String {
16
+ normalize_case_with(line, true)
17
+ }
18
+
19
+ fn normalize_case_with(line: &str, upper: bool) -> String {
11
20
  let trimmed = line.trim_start();
12
21
 
13
22
  // If the line is a Fypp directive, return unchanged.
@@ -107,9 +116,13 @@ pub fn normalize_case(line: &str) -> String {
107
116
  // --- Dot operators: .TRUE., .AND., etc. ---
108
117
  if ch == b'.' && i + 2 < len {
109
118
  if let Some((dot_end, _)) = crate::match_dot_token(bytes, i) {
110
- // Lowercase the entire dot keyword/operator token
119
+ // Normalize the entire dot keyword/operator token
111
120
  let op_str = &line[i..dot_end];
112
- out.push_str(&op_str.to_ascii_lowercase());
121
+ if upper {
122
+ out.push_str(&op_str.to_ascii_uppercase());
123
+ } else {
124
+ out.push_str(&op_str.to_ascii_lowercase());
125
+ }
113
126
  i = dot_end;
114
127
  continue;
115
128
  }
@@ -123,14 +136,29 @@ pub fn normalize_case(line: &str) -> String {
123
136
  }
124
137
  let word = &line[start..i];
125
138
  if is_keyword(word) {
126
- out.push_str(&word.to_ascii_lowercase());
139
+ if upper {
140
+ out.push_str(&word.to_ascii_uppercase());
141
+ } else {
142
+ out.push_str(&word.to_ascii_lowercase());
143
+ }
127
144
  } else {
128
145
  out.push_str(word);
129
146
  }
130
147
  continue;
131
148
  }
132
149
 
133
- // --- Everything else: pass through unchanged ---
150
+ // --- Non-ASCII: copy the whole UTF-8 character unchanged ---
151
+ // (`ch as char` would reinterpret each byte as Latin-1, mojibaking
152
+ // multi-byte sequences.)
153
+ if ch >= 0x80 {
154
+ if let Some(c) = line[i..].chars().next() {
155
+ out.push(c);
156
+ i += c.len_utf8();
157
+ continue;
158
+ }
159
+ }
160
+
161
+ // --- Everything else (ASCII): pass through unchanged ---
134
162
  out.push(ch as char);
135
163
  i += 1;
136
164
  }
@@ -141,6 +141,18 @@ fn find_matching_paren(s: &str, start: usize) -> Option<usize> {
141
141
 
142
142
  /// Classify a Fortran line.
143
143
  fn classify_fortran(trimmed: &str) -> LineKind {
144
+ // Strip an optional numeric statement label (e.g. "10 continue", "100 end if")
145
+ // so the underlying statement is classified. The label is kept in the output;
146
+ // stripping happens for classification only.
147
+ let (trimmed, had_numeric_label) = {
148
+ let re_num_label = re!(NUM_LABEL, r"^\d+\s+");
149
+ if let Some(m) = re_num_label.find(trimmed) {
150
+ (trimmed[m.end()..].trim_start(), true)
151
+ } else {
152
+ (trimmed, false)
153
+ }
154
+ };
155
+
144
156
  // Strip optional label prefix like "outer: "
145
157
  let line = {
146
158
  let re_label = re!(LABEL, r"(?i)^\w+\s*:\s*");
@@ -212,8 +224,20 @@ fn classify_fortran(trimmed: &str) -> LineKind {
212
224
  END_BLOCK,
213
225
  r"(?i)^end\s*(if|do|select|subroutine|function|module|submodule|program|interface|type|block|associate|where|forall|enum|critical|team)\b"
214
226
  );
215
- if re_end_block.is_match(line) {
216
- return LineKind::FortranBlockClose;
227
+ if let Some(m) = re_end_block.find(line) {
228
+ // Fortran has no reserved words: "endif = 5" is an assignment to a
229
+ // variable named "endif", not a block close.
230
+ let rest = line[m.end()..].trim_start();
231
+ if !rest.starts_with('=') {
232
+ // A numeric-labeled "end do" may terminate a non-block labeled DO
233
+ // ("do 10 ... 10 end do"); since "do <label>" does not push an
234
+ // indentation scope, don't pop one here.
235
+ let re_end_do = re!(END_DO_LABELED, r"(?i)^end\s*do\b");
236
+ if had_numeric_label && re_end_do.is_match(line) {
237
+ return LineKind::FortranStatement;
238
+ }
239
+ return LineKind::FortranBlockClose;
240
+ }
217
241
  }
218
242
  // Bare "end" (possibly followed by comment)
219
243
  let re_bare_end = re!(BARE_END, r"(?i)^end\s*(!.*)?$");
@@ -227,8 +251,8 @@ fn classify_fortran(trimmed: &str) -> LineKind {
227
251
  if re_else_if.is_match(line) {
228
252
  return LineKind::FortranContinuation;
229
253
  }
230
- // bare else (possibly followed by comment)
231
- let re_bare_else = re!(BARE_ELSE, r"(?i)^else\s*(!.*)?$");
254
+ // bare else or named "else <construct-name>" (possibly followed by comment)
255
+ let re_bare_else = re!(BARE_ELSE, r"(?i)^else\b(\s+\w+)?\s*(!.*)?$");
232
256
  if re_bare_else.is_match(line) {
233
257
  return LineKind::FortranContinuation;
234
258
  }
@@ -291,6 +315,14 @@ fn classify_fortran(trimmed: &str) -> LineKind {
291
315
  }
292
316
  }
293
317
 
318
+ // Non-block labeled DO ("do 10 i = 1, n") is terminated by the labeled
319
+ // statement ("10 continue"), which is classified as a plain statement, so
320
+ // it must NOT open an indentation scope.
321
+ let re_do_label = re!(DO_LABEL, r"(?i)^do\s+\d+\b");
322
+ if re_do_label.is_match(line) {
323
+ return LineKind::FortranStatement;
324
+ }
325
+
294
326
  // do / do concurrent
295
327
  let re_do = re!(DO_OPEN, r"(?i)^do\b");
296
328
  if re_do.is_match(line) {
@@ -314,10 +346,11 @@ fn classify_fortran(trimmed: &str) -> LineKind {
314
346
  return LineKind::FortranBlockOpen;
315
347
  }
316
348
 
317
- // function (with optional prefixes)
349
+ // function (with optional prefixes; type-spec prefixes may carry a paren
350
+ // group with one level of nesting, e.g. type(point(8)), integer(kind=8))
318
351
  let re_func = re!(
319
352
  FUNC,
320
- r"(?i)^(((pure|elemental|impure|recursive|integer|real|double\s+precision|complex|character|logical|type\s*\([^)]*\))\s+)*)function\b"
353
+ r"(?i)^((((pure|elemental|impure|recursive|module)|(integer|real|double\s+precision|complex|character|logical|type|class)(\s*\(([^()]|\([^()]*\))*\))?)\s+)*)function\b"
321
354
  );
322
355
  if re_func.is_match(line) {
323
356
  return LineKind::FortranBlockOpen;
@@ -361,8 +394,8 @@ fn classify_fortran(trimmed: &str) -> LineKind {
361
394
  return LineKind::FortranStatement;
362
395
  }
363
396
  // type is (...) was already handled in continuations above
364
- // type :: name or type, attrs :: name -> definition (block open)
365
- let re_type_def = re!(TYPE_DEF, r"(?i)^type\b[\s,]");
397
+ // type :: name, type::name, or type, attrs :: name -> definition (block open)
398
+ let re_type_def = re!(TYPE_DEF, r"(?i)^type\s*(::|,|\s)");
366
399
  if re_type_def.is_match(line) {
367
400
  return LineKind::FortranBlockOpen;
368
401
  }
@@ -406,6 +439,12 @@ fn classify_fortran(trimmed: &str) -> LineKind {
406
439
  }
407
440
  }
408
441
 
442
+ // block data [name] (legacy program unit; "end block data" is a closer)
443
+ let re_block_data = re!(BLOCK_DATA, r"(?i)^block\s*data(\s+\w+)?\s*$");
444
+ if re_block_data.is_match(line) {
445
+ return LineKind::FortranBlockOpen;
446
+ }
447
+
409
448
  // block (bare keyword)
410
449
  if lower == "block" {
411
450
  return LineKind::FortranBlockOpen;
@@ -441,6 +480,66 @@ fn classify_fortran(trimmed: &str) -> LineKind {
441
480
  LineKind::FortranStatement
442
481
  }
443
482
 
483
+ /// Split a Fortran line into its top-level statements at semicolons, ignoring
484
+ /// semicolons inside string literals and stopping at an inline comment.
485
+ /// Returns the trimmed pieces (empty pieces are kept so callers can skip them).
486
+ fn split_top_level_statements(line: &str) -> Vec<&str> {
487
+ let bytes = line.as_bytes();
488
+ let mut pieces = Vec::new();
489
+ let mut start = 0;
490
+ let mut in_string = false;
491
+ let mut quote = b' ';
492
+ let mut i = 0;
493
+ while i < bytes.len() {
494
+ let b = bytes[i];
495
+ if in_string {
496
+ if b == quote {
497
+ // Doubled quote is an escape — stay inside the string.
498
+ if i + 1 < bytes.len() && bytes[i + 1] == quote {
499
+ i += 2;
500
+ continue;
501
+ }
502
+ in_string = false;
503
+ }
504
+ i += 1;
505
+ continue;
506
+ }
507
+ match b {
508
+ b'\'' | b'"' => {
509
+ in_string = true;
510
+ quote = b;
511
+ }
512
+ b'!' => break, // inline comment — nothing after it is a statement
513
+ b';' => {
514
+ pieces.push(line[start..i].trim());
515
+ start = i + 1;
516
+ }
517
+ _ => {}
518
+ }
519
+ i += 1;
520
+ }
521
+ pieces.push(line[start..].trim());
522
+ pieces
523
+ }
524
+
525
+ /// Net indentation effect of a line's top-level statements: +1 per block
526
+ /// opener, -1 per block closer, 0 for everything else (continuations,
527
+ /// statements). A self-contained single-line construct nets to 0.
528
+ fn net_block_delta(pieces: &[&str]) -> i32 {
529
+ let mut delta = 0;
530
+ for &piece in pieces {
531
+ if piece.is_empty() {
532
+ continue;
533
+ }
534
+ match classify_fortran(piece) {
535
+ LineKind::FortranBlockOpen => delta += 1,
536
+ LineKind::FortranBlockClose => delta -= 1,
537
+ _ => {}
538
+ }
539
+ }
540
+ delta
541
+ }
542
+
444
543
  /// Classify a logical line.
445
544
  pub fn classify(line: &str) -> LineKind {
446
545
  let trimmed = line.trim();
@@ -479,7 +578,23 @@ pub fn classify(line: &str) -> LineKind {
479
578
  }
480
579
 
481
580
  // 6. Fortran
482
- classify_fortran(trimmed)
581
+ let kind = classify_fortran(trimmed);
582
+
583
+ // A construct that opens AND closes on the same line (e.g.
584
+ // `do d = 1, n; s = s + d; end do`) has a net-zero indent effect. The
585
+ // leading keyword makes `classify_fortran` report a block opener, which
586
+ // would leak an indent level onto every following line. When the opener's
587
+ // matching closer is on the same line (net delta 0), treat the whole line
588
+ // as a plain statement instead — a block opener and a statement both sit at
589
+ // the current depth, so the line's own indentation is unchanged.
590
+ if kind == LineKind::FortranBlockOpen {
591
+ let pieces = split_top_level_statements(trimmed);
592
+ if pieces.len() > 1 && net_block_delta(&pieces) == 0 {
593
+ return LineKind::FortranStatement;
594
+ }
595
+ }
596
+
597
+ kind
483
598
  }
484
599
 
485
600
  /// Extract the scope name from a Fortran block-opening line.
@@ -493,7 +608,10 @@ pub fn classify(line: &str) -> LineKind {
493
608
  /// - `type :: my_type` -> Some("my_type")
494
609
  /// - `type, extends(base) :: child` -> Some("child")
495
610
  pub fn extract_scope_name(line: &str) -> Option<String> {
496
- let trimmed = line.trim();
611
+ // Strip any trailing comment (string-aware) so names are never read from
612
+ // comment text (e.g. `program main ! function test`).
613
+ let stripped = strip_trailing_comment(line.trim());
614
+ let trimmed = stripped.as_str();
497
615
  let lower = trimmed.to_ascii_lowercase();
498
616
 
499
617
  // subroutine name(...) or subroutine name
@@ -508,7 +626,7 @@ pub fn extract_scope_name(line: &str) -> Option<String> {
508
626
  // function name(...) or function name
509
627
  let re_func = re!(
510
628
  SCOPE_FUNC,
511
- r"(?i)(?:(?:pure|elemental|impure|recursive|integer|real|double\s+precision|complex|character|logical|type\s*\([^)]*\))\s+)*function\s+(\w+)"
629
+ r"(?i)(?:(?:pure|elemental|impure|recursive|module|(?:integer|real|double\s+precision|complex|character|logical|type|class)(?:\s*\((?:[^()]|\([^()]*\))*\))?)\s+)*function\s+(\w+)"
512
630
  );
513
631
  if let Some(caps) = re_func.captures(trimmed) {
514
632
  return Some(caps[1].to_string());