Skip to main content

big_code_analysis/
ops.rs

1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
8
9use std::path::{Path, PathBuf};
10
11use serde::Serialize;
12
13use crate::checker::Checker;
14use crate::error::MetricsError;
15use crate::getter::Getter;
16use crate::node::Node;
17use crate::spaces::SpaceKind;
18
19use crate::halstead::{Halstead, HalsteadMaps};
20
21use crate::output::dump_ops::*;
22use crate::traits::*;
23
24/// All operands and operators of a space.
25#[derive(Debug, Clone, Serialize)]
26pub struct Ops {
27    /// The name of a function space.
28    ///
29    /// For the top-level (file-level) `Ops`, this is the file path
30    /// supplied to [`operands_and_operators`] converted via lossy UTF-8
31    /// conversion, so it is always `Some`. Non-UTF-8 path components on
32    /// Linux (or invalid UTF-16 on Windows) become U+FFFD replacement
33    /// characters; in that case [`Ops::name_was_lossy`] is `true` and
34    /// downstream consumers must treat the name as display-only — never
35    /// as a map key or for error correlation.
36    ///
37    /// For nested spaces, `None` means an error occurred in parsing the
38    /// name of the function space from the AST.
39    pub name: Option<String>,
40    /// `true` when [`Ops::name`] was produced by lossy conversion (the
41    /// original path contained non-UTF-8 bytes and was rendered using
42    /// U+FFFD replacement characters). Always `false` for nested spaces
43    /// and for top-level spaces with valid-UTF-8 paths. Skipped from
44    /// JSON output when `false` so existing schemas keep their shape.
45    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
46    pub name_was_lossy: bool,
47    /// The first line of a function space.
48    pub start_line: usize,
49    /// The last line of a function space.
50    pub end_line: usize,
51    /// The space kind.
52    pub kind: SpaceKind,
53    /// All subspaces contained in a function space.
54    pub spaces: Vec<Ops>,
55    /// All operands of a space.
56    pub operands: Vec<String>,
57    /// All operators of a space.
58    pub operators: Vec<String>,
59}
60
61impl Ops {
62    fn new<T: Getter>(node: &Node, code: &[u8], kind: SpaceKind) -> Self {
63        let (start_position, end_position) = match kind {
64            SpaceKind::Unit => {
65                if node.child_count() == 0 {
66                    (0, 0)
67                } else {
68                    (node.start_row() + 1, node.end_row())
69                }
70            }
71            _ => (node.start_row() + 1, node.end_row() + 1),
72        };
73        Self {
74            name: T::get_func_space_name(node, code).map(str::to_owned),
75            name_was_lossy: false,
76            spaces: Vec::new(),
77            kind,
78            start_line: start_position,
79            end_line: end_position,
80            operators: Vec::new(),
81            operands: Vec::new(),
82        }
83    }
84}
85
86#[derive(Debug, Clone)]
87struct State<'a> {
88    ops: Ops,
89    halstead_maps: HalsteadMaps<'a>,
90}
91
92/// Convert `&[u8]` source text to an owned `String`.
93/// Tree-sitter sources are expected to be valid UTF-8; non-UTF-8 bytes
94/// are replaced with the Unicode replacement character to keep the entry
95/// visible (rather than silently dropping it or using a sentinel string
96/// that could collide with a real identifier).
97fn bytes_to_string(b: &[u8]) -> String {
98    String::from_utf8_lossy(b).into_owned()
99}
100
101fn compute_operators_and_operands<T: ParserTrait>(state: &mut State) {
102    state.ops.operators = state
103        .halstead_maps
104        .operators
105        .keys()
106        .map(|k| T::Getter::get_operator_id_as_str(*k).to_owned())
107        .collect();
108
109    // Add primitive-type operators (stored by text in HalsteadMaps)
110    state.ops.operators.extend(
111        state
112            .halstead_maps
113            .primitive_operators
114            .keys()
115            .map(|k| bytes_to_string(k)),
116    );
117
118    state.ops.operands = state
119        .halstead_maps
120        .operands
121        .keys()
122        .map(|k| bytes_to_string(k))
123        .collect();
124}
125
126fn finalize<T: ParserTrait>(state_stack: &mut Vec<State>, diff_level: usize) {
127    if state_stack.is_empty() {
128        return;
129    }
130
131    for _ in 0..diff_level {
132        if state_stack.len() == 1 {
133            break;
134        }
135        let mut state = state_stack
136            .pop()
137            .expect("state_stack verified to have len >= 2");
138        let last_state = state_stack
139            .last_mut()
140            .expect("state_stack verified to have len >= 1 after pop");
141
142        // Populate the child's ops from its HalsteadMaps before
143        // recording it as a sub-space of the parent.
144        compute_operators_and_operands::<T>(&mut state);
145
146        // Merge child's Halstead maps into parent and record child space.
147        last_state.halstead_maps.merge(&state.halstead_maps);
148        last_state.ops.spaces.push(state.ops);
149    }
150
151    // Compute ops for the remaining parent from its fully-merged
152    // HalsteadMaps. This runs once instead of per-iteration, and
153    // produces the deduplicated union of all operators/operands.
154    if let Some(last_state) = state_stack.last_mut() {
155        compute_operators_and_operands::<T>(last_state);
156    }
157}
158
159// Hidden from rustdoc because the signature exposes `ParserTrait`,
160// which is `#[doc(hidden)]` per issue #256. The non-generic
161// `get_ops(&LANG, ...)` entry point is the documented surface.
162#[doc(hidden)]
163/// Retrieves all the operators and operands of a code.
164///
165/// # Errors
166///
167/// The return type carries [`MetricsError::EmptyRoot`] for forward
168/// compatibility, but the walker always produces a top-level [`Ops`]
169/// today (see the variant doc), so this function does not return
170/// `Err` in practice.
171///
172/// # Examples
173///
174/// ```
175/// use std::path::PathBuf;
176///
177/// use big_code_analysis::{operands_and_operators, CppParser, ParserTrait};
178///
179/// # fn main() {
180/// let source_code = "int a = 42;";
181///
182/// // The path to a dummy file used to contain the source code
183/// let path = PathBuf::from("foo.c");
184/// let source_as_vec = source_code.as_bytes().to_vec();
185///
186/// // The parser of the code, in this case a CPP parser
187/// let parser = CppParser::new(source_as_vec, &path, None);
188///
189/// // Returns the operands and operators of each space in a code.
190/// operands_and_operators(&parser, &path).unwrap();
191/// # }
192/// ```
193pub fn operands_and_operators<'a, T: ParserTrait>(
194    parser: &'a T,
195    path: &'a Path,
196) -> Result<Ops, MetricsError> {
197    let code = parser.get_code();
198    let node = parser.get_root();
199    let mut cursor = node.cursor();
200    let mut stack = Vec::new();
201    let mut children = Vec::new();
202    let mut state_stack: Vec<State> = Vec::new();
203    let mut last_level = 0;
204
205    stack.push((node, 0));
206
207    while let Some((node, level)) = stack.pop() {
208        if level < last_level {
209            finalize::<T>(&mut state_stack, last_level - level);
210            last_level = level;
211        }
212
213        let kind = T::Getter::get_space_kind(&node);
214
215        let func_space = T::Checker::is_func(&node) || T::Checker::is_func_space(&node);
216
217        let new_level = if func_space {
218            let state = State {
219                ops: Ops::new::<T::Getter>(&node, code, kind),
220                halstead_maps: HalsteadMaps::new(),
221            };
222            state_stack.push(state);
223            last_level = level + 1;
224            last_level
225        } else {
226            level
227        };
228
229        if let Some(state) = state_stack.last_mut() {
230            T::Halstead::compute(&node, code, &mut state.halstead_maps);
231        }
232
233        cursor.reset(&node);
234        if cursor.goto_first_child() {
235            loop {
236                children.push((cursor.node(), new_level));
237                if !cursor.goto_next_sibling() {
238                    break;
239                }
240            }
241            for child in children.drain(..).rev() {
242                stack.push(child);
243            }
244        }
245    }
246
247    finalize::<T>(&mut state_stack, usize::MAX);
248
249    // Reserved error path: `MetricsError::EmptyRoot` is unreachable
250    // today because every supported language's root node is recognised
251    // as a `func_space` and pushes a state. The `ok_or` is retained so a
252    // future walker change that legitimately drains the stack surfaces
253    // a distinct error variant rather than a bare `None`. See
254    // `MetricsError::EmptyRoot` for the matching variant doc.
255    let mut state = state_stack.pop().ok_or(MetricsError::EmptyRoot)?;
256    // See `FuncSpace::name` rationale in `spaces.rs`: lossy conversion
257    // keeps the top-level `Ops` identifiable for non-UTF-8 paths
258    // rather than collapsing into the empty-root sentinel error. The
259    // `name_was_lossy` flag lets downstream consumers detect and
260    // avoid using the U+FFFD-bearing name as an identifier.
261    let was_lossy = path.to_str().is_none();
262    state.ops.name = Some(path.to_string_lossy().into_owned());
263    state.ops.name_was_lossy = was_lossy;
264    Ok(state.ops)
265}
266
267/// Configuration options for retrieving
268/// all the operands and operators in a code.
269#[derive(Debug)]
270pub struct OpsCfg {
271    /// Path to the file containing the code.
272    pub path: PathBuf,
273}
274
275/// Type tag identifying the operator/operand extraction action; carries no data.
276pub struct OpsCode {
277    _guard: (),
278}
279
280impl Callback for OpsCode {
281    type Res = std::io::Result<()>;
282    type Cfg = OpsCfg;
283
284    fn call<T: ParserTrait>(cfg: Self::Cfg, parser: &T) -> Self::Res {
285        if let Ok(ops) = operands_and_operators(parser, &cfg.path) {
286            dump_ops(&ops)
287        } else {
288            Ok(())
289        }
290    }
291}
292
293#[cfg(test)]
294#[allow(
295    clippy::float_cmp,
296    clippy::cast_precision_loss,
297    clippy::cast_possible_truncation,
298    clippy::cast_sign_loss,
299    clippy::similar_names,
300    clippy::doc_markdown,
301    clippy::needless_raw_string_hashes,
302    clippy::too_many_lines
303)]
304mod tests {
305    use std::path::PathBuf;
306
307    use crate::{LANG, get_ops};
308
309    #[inline]
310    fn check_ops(
311        lang: LANG,
312        source: &str,
313        file: &str,
314        correct_operators: &mut [&str],
315        correct_operands: &mut [&str],
316    ) {
317        let path = PathBuf::from(file);
318        let mut trimmed_bytes = source.trim_end().trim_matches('\n').as_bytes().to_vec();
319        trimmed_bytes.push(b'\n');
320        let ops = get_ops(&lang, trimmed_bytes, &path, None).unwrap();
321
322        let mut operators_str: Vec<&str> = ops.operators.iter().map(AsRef::as_ref).collect();
323        let mut operands_str: Vec<&str> = ops.operands.iter().map(AsRef::as_ref).collect();
324
325        // Sorting out operators because they are returned in arbitrary order
326        operators_str.sort_unstable();
327        correct_operators.sort_unstable();
328
329        assert_eq!(&operators_str[..], correct_operators);
330
331        // Sorting out operands because they are returned in arbitrary order
332        operands_str.sort_unstable();
333        correct_operands.sort_unstable();
334
335        assert_eq!(&operands_str[..], correct_operands);
336    }
337
338    #[test]
339    fn python_ops() {
340        check_ops(
341            LANG::Python,
342            "if True:
343                 a = 1 + 2",
344            "foo.py",
345            &mut ["if", "=", "+"],
346            &mut ["True", "a", "1", "2"],
347        );
348    }
349
350    #[test]
351    fn python_function_ops() {
352        check_ops(
353            LANG::Python,
354            "def foo():
355                 def bar():
356                     def toto():
357                        a = 1 + 1
358                     b = 2 + a
359                 c = 3 + 3",
360            "foo.py",
361            &mut ["def", "=", "+"],
362            &mut ["foo", "bar", "toto", "a", "b", "c", "1", "2", "3"],
363        );
364    }
365
366    #[test]
367    fn cpp_ops() {
368        check_ops(
369            LANG::Cpp,
370            "int a, b, c;
371             float avg;
372             avg = (a + b + c) / 3;",
373            "foo.c",
374            &mut ["int", "float", "()", "=", "+", "/", ",", ";"],
375            &mut ["a", "b", "c", "avg", "3"],
376        );
377    }
378
379    #[test]
380    fn cpp_function_ops() {
381        check_ops(
382            LANG::Cpp,
383            "main()
384            {
385              int a, b, c, avg;
386              scanf(\"%d %d %d\", &a, &b, &c);
387              avg = (a + b + c) / 3;
388              printf(\"avg = %d\", avg);
389            }",
390            "foo.c",
391            &mut ["()", "{}", "int", "&", "=", "+", "/", ",", ";"],
392            &mut [
393                "main",
394                "a",
395                "b",
396                "c",
397                "avg",
398                "scanf",
399                "\"%d %d %d\"",
400                "3",
401                "printf",
402                "\"avg = %d\"",
403            ],
404        );
405    }
406
407    #[test]
408    fn rust_ops() {
409        check_ops(
410            LANG::Rust,
411            "let: usize a = 5; let b: f32 = 7.0; let c: i32 = 3;",
412            "foo.rs",
413            &mut ["let", "usize", "=", ";", "f32", "i32"],
414            &mut ["a", "b", "c", "5", "7.0", "3"],
415        );
416    }
417
418    #[test]
419    fn rust_function_ops() {
420        check_ops(
421            LANG::Rust,
422            "fn main() {
423              let a = 5; let b = 5; let c = 5;
424              let avg = (a + b + c) / 3;
425              println!(\"{}\", avg);
426            }",
427            "foo.rs",
428            &mut ["fn", "()", "{}", "let", "=", "+", "/", ";", "!", ","],
429            &mut ["main", "a", "b", "c", "avg", "5", "3", "println", "\"{}\""],
430        );
431    }
432
433    #[test]
434    fn javascript_ops() {
435        check_ops(
436            LANG::Javascript,
437            "var a, b, c, avg;
438             let x = 1;
439             a = 5; b = 5; c = 5;
440             avg = (a + b + c) / 3;
441             console.log(\"{}\", avg);",
442            "foo.js",
443            &mut ["()", "var", "let", "=", "+", "/", ",", ".", ";"],
444            &mut [
445                "a",
446                "b",
447                "c",
448                "avg",
449                "x",
450                "1",
451                "3",
452                "5",
453                "console.log",
454                "console",
455                "log",
456                "\"{}\"",
457            ],
458        );
459    }
460
461    #[test]
462    fn javascript_function_ops() {
463        check_ops(
464            LANG::Javascript,
465            "function main() {
466              var a, b, c, avg;
467              let x = 1;
468              a = 5; b = 5; c = 5;
469              avg = (a + b + c) / 3;
470              console.log(\"{}\", avg);
471            }",
472            "foo.js",
473            &mut [
474                "function", "()", "{}", "var", "let", "=", "+", "/", ",", ".", ";",
475            ],
476            &mut [
477                "main",
478                "a",
479                "b",
480                "c",
481                "avg",
482                "x",
483                "1",
484                "3",
485                "5",
486                "console.log",
487                "console",
488                "log",
489                "\"{}\"",
490            ],
491        );
492    }
493
494    #[test]
495    fn mozjs_ops() {
496        check_ops(
497            LANG::Mozjs,
498            "var a, b, c, avg;
499             let x = 1;
500             a = 5; b = 5; c = 5;
501             avg = (a + b + c) / 3;
502             console.log(\"{}\", avg);",
503            "foo.js",
504            &mut ["()", "var", "let", "=", "+", "/", ",", ".", ";"],
505            &mut [
506                "a",
507                "b",
508                "c",
509                "avg",
510                "x",
511                "1",
512                "3",
513                "5",
514                "console.log",
515                "console",
516                "log",
517                "\"{}\"",
518            ],
519        );
520    }
521
522    #[test]
523    fn mozjs_function_ops() {
524        check_ops(
525            LANG::Mozjs,
526            "function main() {
527              var a, b, c, avg;
528              let x = 1;
529              a = 5; b = 5; c = 5;
530              avg = (a + b + c) / 3;
531              console.log(\"{}\", avg);
532            }",
533            "foo.js",
534            &mut [
535                "function", "()", "{}", "var", "let", "=", "+", "/", ",", ".", ";",
536            ],
537            &mut [
538                "main",
539                "a",
540                "b",
541                "c",
542                "avg",
543                "x",
544                "1",
545                "3",
546                "5",
547                "console.log",
548                "console",
549                "log",
550                "\"{}\"",
551            ],
552        );
553    }
554
555    #[test]
556    fn typescript_ops() {
557        // Issue #313: the `: string` annotation's `String2` child now
558        // emits a `"string"` operand alongside the `string`
559        // primitive-typed operator (PredefinedType wrapper). Other
560        // type-keyword annotations (`: number`, `: boolean`) are not
561        // string-named kinds, so they only contribute an operator.
562        check_ops(
563            LANG::Typescript,
564            "var a, b, c, avg;
565             let age: number = 32;
566             let name: string = \"John\"; let isUpdated: boolean = true;
567             a = 5; b = 5; c = 5;
568             avg = (a + b + c) / 3;
569             console.log(\"{}\", avg);",
570            "foo.ts",
571            &mut [
572                "()", "var", "let", "string", "number", "boolean", ":", "=", "+", "/", ",", ".",
573                ";",
574            ],
575            &mut [
576                "a",
577                "b",
578                "c",
579                "avg",
580                "age",
581                "name",
582                "isUpdated",
583                "32",
584                "\"John\"",
585                "true",
586                "3",
587                "5",
588                "console.log",
589                "console",
590                "log",
591                "\"{}\"",
592                "string",
593            ],
594        );
595    }
596
597    #[test]
598    fn typescript_function_ops() {
599        // Issue #313: see `typescript_ops` — the `string` type keyword
600        // appears as both an operator (primitive-typed) and an operand
601        // (text `"string"`) once Checker/Getter parity is enforced.
602        check_ops(
603            LANG::Typescript,
604            "function main() {
605              var a, b, c, avg;
606              let age: number = 32;
607              let name: string = \"John\"; let isUpdated: boolean = true;
608              a = 5; b = 5; c = 5;
609              avg = (a + b + c) / 3;
610              console.log(\"{}\", avg);
611            }",
612            "foo.ts",
613            &mut [
614                "function", "()", "{}", "var", "let", "string", "number", "boolean", ":", "=", "+",
615                "/", ",", ".", ";",
616            ],
617            &mut [
618                "main",
619                "a",
620                "b",
621                "c",
622                "avg",
623                "age",
624                "name",
625                "isUpdated",
626                "32",
627                "\"John\"",
628                "true",
629                "3",
630                "5",
631                "console.log",
632                "console",
633                "log",
634                "\"{}\"",
635                "string",
636            ],
637        );
638    }
639
640    #[test]
641    fn tsx_ops() {
642        // Issue #313: TSX exposes the `: string` type-keyword child as
643        // `String3` (vs. TS's `String2`); both are now in the operand
644        // classification, so `"string"` appears as a TSX operand for
645        // the same reason as the TS case above.
646        check_ops(
647            LANG::Tsx,
648            "var a, b, c, avg;
649             let age: number = 32;
650             let name: string = \"John\"; let isUpdated: boolean = true;
651             a = 5; b = 5; c = 5;
652             avg = (a + b + c) / 3;
653             console.log(\"{}\", avg);",
654            "foo.ts",
655            &mut [
656                "()", "var", "let", "string", "number", "boolean", ":", "=", "+", "/", ",", ".",
657                ";",
658            ],
659            &mut [
660                "a",
661                "b",
662                "c",
663                "avg",
664                "age",
665                "name",
666                "isUpdated",
667                "32",
668                "\"John\"",
669                "true",
670                "3",
671                "5",
672                "console.log",
673                "console",
674                "log",
675                "\"{}\"",
676                "string",
677            ],
678        );
679    }
680
681    #[test]
682    fn tsx_function_ops() {
683        // Issue #313: see `tsx_ops` — TSX::String3 (type-keyword
684        // `string`) is now an operand.
685        check_ops(
686            LANG::Tsx,
687            "function main() {
688              var a, b, c, avg;
689              let age: number = 32;
690              let name: string = \"John\"; let isUpdated: boolean = true;
691              a = 5; b = 5; c = 5;
692              avg = (a + b + c) / 3;
693              console.log(\"{}\", avg);
694            }",
695            "foo.ts",
696            &mut [
697                "function", "()", "{}", "var", "let", "string", "number", "boolean", ":", "=", "+",
698                "/", ",", ".", ";",
699            ],
700            &mut [
701                "main",
702                "a",
703                "b",
704                "c",
705                "avg",
706                "age",
707                "name",
708                "isUpdated",
709                "32",
710                "\"John\"",
711                "true",
712                "3",
713                "5",
714                "console.log",
715                "console",
716                "log",
717                "\"{}\"",
718                "string",
719            ],
720        );
721    }
722
723    #[test]
724    fn java_ops() {
725        check_ops(
726            LANG::Java,
727            "public class Main {
728                public static void main(string args[]) {
729                      int a, b, c, avg;
730                      a = 5; b = 5; c = 5;
731                      avg = (a + b + c) / 3;
732                      MessageFormat.format(\"{0}\", avg);
733                    }
734                }",
735            "foo.java",
736            &mut [
737                "{}", "void", "()", "[]", ",", ".", ";", "int", "=", "+", "/",
738            ],
739            &mut [
740                "Main",
741                "main",
742                "args",
743                "a",
744                "b",
745                "c",
746                "avg",
747                "5",
748                "3",
749                "MessageFormat",
750                "format",
751                "\"{0}\"",
752            ],
753        );
754    }
755
756    #[test]
757    fn java_primitive_ops() {
758        check_ops(
759            LANG::Java,
760            "public class Prims {
761                byte a = 1;
762                short b = 2;
763                int c = 3;
764                long d = 4;
765                char e = 'x';
766                float f = 1.0f;
767                double g = 2.0;
768                boolean h = true;
769                boolean i = false;
770            }",
771            "foo.java",
772            // All 8 primitive-type keywords must appear as distinct operators.
773            // true/false appear as operands.
774            &mut [
775                "{}",
776                ";",
777                "=",
778                "byte",
779                "short",
780                "int",
781                "long",
782                "char",
783                "float",
784                "double",
785                "boolean_type",
786            ],
787            &mut [
788                "Prims", "a", "b", "c", "d", "e", "f", "g", "h", "i", "1", "2", "3", "4", "'x'",
789                "1.0f", "2.0", "true", "false",
790            ],
791        );
792    }
793
794    /// Regression for issue #128 — non-UTF-8 paths must not collapse the
795    /// top-level `Ops::name` into `None`, which is reserved for AST-name
796    /// parse failures.
797    #[cfg(unix)]
798    #[test]
799    fn non_utf8_path_yields_lossy_top_level_name() {
800        use std::ffi::OsStr;
801        use std::os::unix::ffi::OsStrExt;
802
803        let raw_bytes: &[u8] = b"foo_\xFF\xFE_bar.py";
804        let path = PathBuf::from(OsStr::from_bytes(raw_bytes));
805        assert!(
806            path.to_str().is_none(),
807            "test premise broken: path must be non-UTF-8 for this test to be meaningful"
808        );
809
810        let ops = get_ops(&LANG::Python, b"a = 1\n".to_vec(), &path, None)
811            .expect("get_ops must yield a top-level Ops");
812
813        let name = ops
814            .name
815            .as_deref()
816            .expect("top-level Ops name must be Some, not the parse-error sentinel None");
817        assert!(
818            name.contains('\u{FFFD}'),
819            "expected U+FFFD replacement char in lossy name, got {name:?}"
820        );
821        assert!(
822            name.starts_with("foo_") && name.ends_with("_bar.py"),
823            "lossy name must preserve the surrounding ASCII bytes, got {name:?}"
824        );
825        assert!(
826            ops.name_was_lossy,
827            "name_was_lossy must be true when the source path was non-UTF-8"
828        );
829    }
830
831    /// Top-level `Ops` with valid UTF-8 paths must NOT have
832    /// `name_was_lossy` set.
833    #[test]
834    fn utf8_path_does_not_set_name_was_lossy() {
835        let path = PathBuf::from("foo.py");
836        let ops = get_ops(&LANG::Python, b"a = 1\n".to_vec(), &path, None)
837            .expect("get_ops must yield a top-level Ops");
838        assert!(
839            !ops.name_was_lossy,
840            "name_was_lossy must be false for valid-UTF-8 paths"
841        );
842    }
843}