• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

moonbitlang / x / 867

18 Jun 2026 06:38AM UTC coverage: 88.523% (+1.7%) from 86.827%
867

push

github

myfreess
path: document Node-compatible dispatch

2553 of 2884 relevant lines covered (88.52%)

338.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.83
/path/posix/path.mbt
1
// Copyright 2025 International Digital Economy Academy
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//     http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14

15
///|
16
/// A newtype wrapper provide path operation methods.
17
pub(all) struct Path(String) derive(Eq, Debug)
18

19
///|
20
fn is_posix_sep_at(path : String, index : Int) -> Bool {
21
  path.get_char(index) == Some('/')
390✔
22
}
23

24
///|
25
pub impl Show for Path with fn output(path, logger) {
26
  logger.write_object(path.0)
×
27
}
28

29
///|
30
pub impl Show for Path with fn to_string(path) {
31
  path.0
125✔
32
}
33

34
///|
35
/// Returns the last path component of the given path.
36
///
37
/// This follows Node `path.posix.basename`: trailing separators are ignored and
38
/// repeated separators are not normalized before scanning.
39
/// 
40
/// ```moonbit check
41
/// test {
42
///   let path : Path = "usr/local/"
43
///   inspect(path.basename(), content="local")
44
/// }
45
/// ```
46
pub fn Path::basename(path : Path) -> StringView {
47
  let path = path.0
51✔
48
  let mut start = 0
49
  let mut end = -1
50
  let mut matched_slash = true
51
  for i = path.length() - 1; i >= 0; i = i - 1 {
51✔
52
    if is_posix_sep_at(path, i) {
304✔
53
      if !matched_slash {
39✔
54
        start = i + 1
22✔
55
        break
56
      }
57
    } else if end == -1 {
265✔
58
      matched_slash = false
46✔
59
      end = i + 1
60
    }
61
  }
62
  if end == -1 {
63
    ""
5✔
64
  } else {
65
    path[start:end]
46✔
66
  }
67
}
68

69
///|
70
/// Returns the directory portion of the given path.
71
///
72
/// This follows Node `path.posix.dirname`: it ignores trailing separators while
73
/// preserving repeated separators that participate in the directory portion.
74
/// 
75
/// ```moonbit check
76
/// test {
77
///   let path : Path = "usr/local/"
78
///   inspect(path.dirname(), content="usr")
79
/// }
80
/// ```
81
pub fn Path::dirname(path : Path) -> Path {
82
  let path = path.0
24✔
83
  if path.is_empty() {
24✔
84
    return "."
1✔
85
  }
86
  let has_root = is_posix_sep_at(path, 0)
23✔
87
  let mut end = -1
88
  let mut matched_slash = true
89
  for i = path.length() - 1; i >= 1; i = i - 1 {
23✔
90
    if is_posix_sep_at(path, i) {
63✔
91
      if !matched_slash {
27✔
92
        end = i
15✔
93
        break
94
      }
95
    } else {
96
      matched_slash = false
36✔
97
    }
98
  }
99
  if end == -1 {
100
    if has_root {
8✔
101
      "/"
5✔
102
    } else {
103
      "."
3✔
104
    }
105
  } else if has_root && end == 1 {
15✔
106
    "//"
1✔
107
  } else {
108
    path[0:end].to_owned()
14✔
109
  }
110
}
111

112
///|
113
/// Returns the extension of the last path component.
114
///
115
/// This follows Node `path.posix.extname` dotfile rules: a leading dot by
116
/// itself does not start an extension.
117
/// 
118
/// ```moonbit check
119
/// test {
120
///   let path : Path = "archive.tar.gz"
121
///   inspect(path.extname(), content=".gz")
122
/// }
123
/// ```
124
pub fn Path::extname(path : Path) -> StringView {
125
  let basename = Path::basename(path)
32✔
126
  if basename == "." || basename == ".." {
127
    return ""
4✔
128
  }
129
  match basename.rev_find(".") {
28✔
130
    None => ""
7✔
131
    Some(0) => ""
4✔
132
    Some(idx) => basename[idx:]
17✔
133
  }
134
}
135

136
///|
137
/// Returns whether the given path is absolute.
138
/// 
139
/// edge cases: when path is empty, return false
140
/// 
141
/// ```moonbit check
142
/// test {
143
///   let path1 : Path = "/usr/local/bin"
144
///   let path2 : Path = "usr/local/bin"
145
///   json_inspect(path1.is_absolute(), content=true)
146
///   json_inspect(path2.is_absolute(), content=false)
147
/// }
148
/// ```
149
pub fn Path::is_absolute(path : Path) -> Bool {
150
  let path = path.0
82✔
151
  match path {
82✔
152
    ['/', ..] => true
65✔
153
    _ => false
17✔
154
  }
155
}
156

157
///|
158
/// Concatenates two path strings with `/`, then normalizes the result.
159
///
160
/// Unlike `resolve`, an absolute right-hand path does not override `lhs`.
161
/// 
162
/// ```mbt check
163
/// test {
164
///   assert_eq(Path::join("/usr/local", "bin"), "/usr/local/bin")
165
///   assert_eq(Path::join("/usr/local", "/usr"), "/usr/local/usr")
166
///   assert_eq(Path::join("/usr/local", ""), "/usr/local")
167
///   assert_eq(Path::join("", "local/bin"), "local/bin")
168
///   assert_eq(Path::join("", ""), ".")
169
/// }
170
/// ```
171
pub fn Path::join(lhs : Path, rhs : Path) -> Path {
172
  let lhs = lhs.0
34✔
173
  let rhs = rhs.0
174
  if lhs.is_empty() && rhs.is_empty() {
5✔
175
    return "."
2✔
176
  }
177
  if lhs.is_empty() {
32✔
178
    return Path::normalize(rhs)
3✔
179
  }
180
  if rhs.is_empty() {
29✔
181
    return Path::normalize(lhs)
2✔
182
  }
183
  let builder = StringBuilder::new()
27✔
184
  builder.write_stringview(lhs)
27✔
185
  builder.write_char('/')
27✔
186
  builder.write_stringview(rhs)
27✔
187
  Path::normalize(builder.to_string())
27✔
188
}
189

190
///|
191
/// 1. resolve `.` by directly removing it
192
/// 2. resolve `..` by removing the preceding path component if exists, otherwise keep it.
193
/// 3. remove redundant slashes 
194
/// 4. preserve trailing slash
195
/// 
196
/// edge cases:
197
///   1. when path is empty, return `.`
198
///   2. leading `//` is normalized to `/`, following Node `path.posix`.
199
/// 
200
/// ```moonbit check
201
/// test {
202
///   let path : Path = "/usr//local/../bin/"
203
///   inspect(path.normalize(), content="/usr/bin/")
204
/// }
205
/// ```
206
pub fn Path::normalize(path : Path) -> Path {
207
  UnixPath::parse(path.0).to_string()
128✔
208
}
209

210
///|
211
fn trim_resolved_trailing_separators(path : Path) -> Path {
212
  let raw = path.0
54✔
213
  if raw is [.. rest, lastchar] && lastchar == '/' && !rest.is_empty() {
10✔
214
    Path(rest.to_owned())
3✔
215
  } else {
216
    path
51✔
217
  }
218
}
219

220
///|
221
/// Return the `to` relative path when `from` is the current working directory.
222
/// 
223
/// property: `@path.join(from,@path.relative(from,to)) == @path.normalize(to)`
224
/// 
225
/// edge cases: 
226
///  1. when `@path.normalize(from) == @path.normalize(to)`, return empty string
227
///  2. when `from` and `to` have different prefixes, return empty string
228
/// 
229
/// Warning: cwd is already resolve symbolic link and normalized, but path doesn't.
230
/// 
231
/// ```moonbit check
232
/// test {
233
///   let base : Path = "/usr/local/bin"
234
///   let path : Path = "/usr/local/share/doc"
235
///   inspect(path.relative(base~), content="../share/doc")
236
/// }
237
/// ```
238
pub fn Path::relative(path : Path, base~ : Path) -> Path {
239
  let from = UnixPath::parse(Path::resolve(base).0)
21✔
240
  let to = UnixPath::parse(Path::resolve(path).0)
21✔
241
  if from.prefix == to.prefix {
242
    let mut cnt = 0
21✔
243
    // remove the common path component prefix
244
    for left = from.components[:], right = to.components[:] {
245
      match (left, right) {
55✔
246
        ([x, .. xs], [y, .. ys]) if x == y => {
34✔
247
          cnt += 1
248
          continue xs, ys
249
        }
250
        _ => break
21✔
251
      }
252
    }
253
    let components : Array[StringView] = []
254
    let goto_parent_cnt = from.components.length() - cnt
21✔
255
    for _ in 0..<goto_parent_cnt {
256
      components.push("..")
15✔
257
    }
258
    let rest_path = to.components[cnt:]
259
    for comp in rest_path {
260
      components.push(comp)
13✔
261
    }
262
    if components.is_empty() {
21✔
263
      ""
6✔
264
    } else {
265
      let path = UnixPath::{
15✔
266
        prefix: None,
267
        trailing_separator: false,
268
        components,
269
      }
270
      path.to_string()
15✔
271
    }
272
  } else {
273
    ""
×
274
  }
275
}
276

277
///|
278
/// 1. if path is already absolute, return path normalized.
279
/// 2. if path is relative, join current working directory and path, and then normalize it.
280
///
281
/// Warning: cwd is already resolve symbolic link and normalized, but path doesn't.
282
/// 
283
pub fn Path::resolve(path : Path) -> Path {
284
  let resolved = if Path::is_absolute(path) {
54✔
285
    Path::normalize(path)
54✔
286
  } else {
287
    Path::join(@env.current_dir().unwrap(), path) |> Path::normalize
×
288
  }
289
  trim_resolved_trailing_separators(resolved)
54✔
290
}
291

292
///|
293
/// OS platform specific path delimiter for environment variables. e.g., PATH
294
pub let delimiter : Char = ':'
295

296
///|
297
/// OS platform specific path component separator.
298
pub let sep : Char = '/'
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc