Skip to main content

fallow_graph/graph/
public_exports.rs

1//! Exports-aware public-export-key computation (the hard 80% of 6.A).
2//!
3//! Given the set of *public-API entry points* (the `package.json` `exports`-mapped
4//! modules plus the no-`exports` source-index fallback; computed in core's
5//! `public_api_package_entry_points`, which already encodes rule R4), this
6//! resolves the set of export symbols reachable through that public surface and
7//! returns one stable `"<rel_path>::<name>"` key per public export.
8//!
9//! A name is public when it resolves to one unique value binding on a public-API
10//! entry point. Candidates come from the entry's own exports and its `export *`
11//! closure. Shadowed and ambiguous star names are not public bindings.
12//!
13//! Keying on the surface AS EXPOSED (the entry's own name, e.g. `index.js::pub`),
14//! not the origin's internal name (`src/impl.ts::pub`), is what makes the delta
15//! exports-aware and avoids double-counting one symbol on both the barrel and
16//! the origin. A symbol re-exported only through an INTERNAL barrel that is not
17//! in `exports` never resolves on a public entry, so it produces
18//! ZERO public-API delta (the Aisha repro); one re-exported through the
19//! `exports`-mapped entry lands on that entry once (exactly one). This mirrors
20//! the exports-aware reachability the `unprovided-inject` and
21//! `unrendered-component` detectors use, kept in the graph crate so the review
22//! brief (cli) can call it directly off the retained graph.
23
24use std::path::Path;
25
26use super::relativize;
27
28use fallow_types::discover::FileId;
29use rustc_hash::FxHashSet;
30
31use super::{EffectiveExportBinding, EffectiveExportResolution, ExportNamespace, ModuleGraph};
32
33/// Direct declaration exposed through at least one public package entry.
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub struct PublicExportOrigin {
36    file_id: FileId,
37    export_name: String,
38}
39
40impl PublicExportOrigin {
41    /// Module that owns the public declaration.
42    #[must_use]
43    pub const fn file_id(&self) -> FileId {
44        self.file_id
45    }
46
47    /// Name of the declaration in its owning module.
48    #[must_use]
49    pub fn export_name(&self) -> &str {
50        &self.export_name
51    }
52}
53
54impl ModuleGraph {
55    /// Compute the set of public-export keys reachable through the given
56    /// `public_api_entry_points` (an exports-aware set; see module docs).
57    ///
58    /// Keys are `"<root-relative forward-slashed path>::<export name>"`.
59    /// Type-only exports are skipped: a type erased at build carries no runtime
60    /// contract, so it never widens the public *value* surface that 6.A tracks.
61    #[must_use]
62    pub fn public_export_keys(
63        &self,
64        public_api_entry_points: &FxHashSet<FileId>,
65        root: &Path,
66    ) -> FxHashSet<String> {
67        let candidate_names = self.public_value_export_names(public_api_entry_points);
68        let mut keys: FxHashSet<String> = FxHashSet::default();
69
70        for entry_id in public_api_entry_points {
71            let Some(entry) = self.modules.get(entry_id.0 as usize) else {
72                continue;
73            };
74            let rel = relativize(&entry.path, root);
75            for name in &candidate_names {
76                if matches!(
77                    self.resolve_export(*entry_id, name, ExportNamespace::Value),
78                    EffectiveExportResolution::Unique(_)
79                ) {
80                    keys.insert(format!("{rel}::{name}"));
81                }
82            }
83        }
84        keys
85    }
86
87    /// Resolve the direct declarations exposed through public package entries.
88    /// Shadowed and ambiguous star candidates contribute no origins.
89    #[must_use]
90    pub fn public_export_origins(
91        &self,
92        public_api_entry_points: &FxHashSet<FileId>,
93    ) -> FxHashSet<PublicExportOrigin> {
94        self.public_export_origins_in_namespace(public_api_entry_points, ExportNamespace::Value)
95    }
96
97    /// Resolve direct declarations exposed through public package entries in
98    /// one export namespace.
99    ///
100    /// Type-only public surfaces matter to structural consumers such as class
101    /// member analysis, while runtime consumers can continue to request only
102    /// the value namespace.
103    #[must_use]
104    pub fn public_export_origins_in_namespace(
105        &self,
106        public_api_entry_points: &FxHashSet<FileId>,
107        namespace: ExportNamespace,
108    ) -> FxHashSet<PublicExportOrigin> {
109        self.public_export_declaration_bindings_in_namespace(public_api_entry_points, namespace)
110            .into_iter()
111            .filter_map(|binding| self.export_binding_origin(binding))
112            .map(|origin| PublicExportOrigin {
113                file_id: origin.file_id(),
114                export_name: origin.export().name.to_string(),
115            })
116            .collect()
117    }
118
119    /// Unique value bindings exposed through at least one public package entry.
120    #[must_use]
121    pub fn public_export_bindings(
122        &self,
123        public_api_entry_points: &FxHashSet<FileId>,
124    ) -> FxHashSet<EffectiveExportBinding> {
125        self.public_export_bindings_in_namespace(public_api_entry_points, ExportNamespace::Value)
126    }
127
128    fn public_export_bindings_in_namespace(
129        &self,
130        public_api_entry_points: &FxHashSet<FileId>,
131        namespace: ExportNamespace,
132    ) -> FxHashSet<EffectiveExportBinding> {
133        let candidate_names = self.public_export_names(public_api_entry_points, namespace);
134        public_api_entry_points
135            .iter()
136            .flat_map(|entry| {
137                candidate_names.iter().filter_map(|name| {
138                    match self.resolve_export(*entry, name, namespace) {
139                        EffectiveExportResolution::Unique(binding) => Some(binding),
140                        EffectiveExportResolution::Missing
141                        | EffectiveExportResolution::Ambiguous => None,
142                    }
143                })
144            })
145            .collect()
146    }
147
148    /// Direct declaration bindings exposed through public entries, including
149    /// declarations nested under public namespace-object exports.
150    #[must_use]
151    pub fn public_export_declaration_bindings(
152        &self,
153        public_api_entry_points: &FxHashSet<FileId>,
154    ) -> FxHashSet<EffectiveExportBinding> {
155        self.public_export_declaration_bindings_in_namespace(
156            public_api_entry_points,
157            ExportNamespace::Value,
158        )
159    }
160
161    fn public_export_declaration_bindings_in_namespace(
162        &self,
163        public_api_entry_points: &FxHashSet<FileId>,
164        namespace: ExportNamespace,
165    ) -> FxHashSet<EffectiveExportBinding> {
166        let mut declarations = FxHashSet::default();
167        let mut visited = FxHashSet::default();
168        let mut stack: Vec<_> = self
169            .public_export_bindings_in_namespace(public_api_entry_points, namespace)
170            .into_iter()
171            .collect();
172        while let Some(binding) = stack.pop() {
173            if !visited.insert(binding) {
174                continue;
175            }
176            if let Some(source) = binding.namespace_source() {
177                stack.extend(self.unique_export_bindings(source, namespace));
178            } else if self.export_binding_origin(binding).is_some() {
179                declarations.insert(binding);
180            }
181        }
182        declarations
183    }
184
185    fn public_value_export_names(
186        &self,
187        public_api_entry_points: &FxHashSet<FileId>,
188    ) -> FxHashSet<String> {
189        self.public_export_names(public_api_entry_points, ExportNamespace::Value)
190    }
191
192    fn public_export_names(
193        &self,
194        public_api_entry_points: &FxHashSet<FileId>,
195        namespace: ExportNamespace,
196    ) -> FxHashSet<String> {
197        let star_targets = self.public_star_re_export_targets(public_api_entry_points);
198        let mut names: FxHashSet<String> = public_api_entry_points
199            .iter()
200            .chain(star_targets.iter())
201            .filter_map(|id| self.modules.get(id.0 as usize))
202            .flat_map(|module| &module.exports)
203            .filter(|export| namespace == ExportNamespace::Type || !export.is_type_only)
204            .map(|export| export.name.to_string())
205            .collect();
206        names.insert("default".to_string());
207        names
208    }
209
210    /// The `export *` closure rooted at the public-API entry points: every module
211    /// reachable through a chain of `export * from './x'` edges starting from a
212    /// public entry. Their exported names are candidates for effective
213    /// resolution on each public entry.
214    fn public_star_re_export_targets(
215        &self,
216        public_api_entry_points: &FxHashSet<FileId>,
217    ) -> FxHashSet<FileId> {
218        let mut targets: FxHashSet<FileId> = public_api_entry_points
219            .iter()
220            .filter_map(|id| self.modules.get(id.0 as usize))
221            .flat_map(|module| {
222                module
223                    .re_exports
224                    .iter()
225                    .filter(|re| re.exported_name == "*")
226                    .map(|re| re.source_file)
227            })
228            .collect();
229
230        let mut stack: Vec<FileId> = targets.iter().copied().collect();
231        while let Some(id) = stack.pop() {
232            let Some(module) = self.modules.get(id.0 as usize) else {
233                continue;
234            };
235            for re in module
236                .re_exports
237                .iter()
238                .filter(|re| re.exported_name == "*")
239            {
240                if targets.insert(re.source_file) {
241                    stack.push(re.source_file);
242                }
243            }
244        }
245        targets
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::resolve::{ResolveResult, ResolvedImport, ResolvedModule, ResolvedReExport};
253    use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource};
254    use fallow_types::extract::{
255        ExportInfo, ExportName, ImportInfo, ImportedName, ReExportInfo, VisibilityTag,
256    };
257    use std::path::PathBuf;
258
259    fn file(id: u32, path: &str) -> DiscoveredFile {
260        DiscoveredFile {
261            id: FileId(id),
262            path: PathBuf::from(path),
263            size_bytes: 10,
264        }
265    }
266
267    fn named_export(name: &str) -> ExportInfo {
268        ExportInfo {
269            name: ExportName::Named(name.to_string()),
270            local_name: Some(name.to_string()),
271            is_type_only: false,
272            visibility: VisibilityTag::None,
273            expected_unused_reason: None,
274            span: oxc_span::Span::new(0, 20),
275            members: vec![],
276            is_side_effect_used: false,
277            super_class: None,
278        }
279    }
280
281    fn re_export(imported: &str, exported: &str, target: FileId) -> ResolvedReExport {
282        ResolvedReExport {
283            info: ReExportInfo {
284                source: "./impl".to_string(),
285                imported_name: imported.to_string(),
286                exported_name: exported.to_string(),
287                is_type_only: false,
288                span: oxc_span::Span::new(0, 10),
289                statement_span: oxc_span::Span::new(0, 0),
290                source_span: oxc_span::Span::new(0, 0),
291            },
292            target: ResolveResult::InternalModule(target),
293        }
294    }
295
296    fn named_import(name: &str, target: FileId) -> ResolvedImport {
297        ResolvedImport {
298            info: ImportInfo {
299                source: "./x".to_string(),
300                imported_name: ImportedName::Named(name.to_string()),
301                local_name: name.to_string(),
302                is_type_only: false,
303                is_type_only_star: false,
304                from_style: false,
305                span: oxc_span::Span::new(0, 10),
306                source_span: oxc_span::Span::default(),
307            },
308            target: ResolveResult::InternalModule(target),
309        }
310    }
311
312    /// index (0, the exports entry) re-exports `pub` from impl (1, NOT public);
313    /// internal-barrel (2) re-exports `priv` from impl. consumer (3) imports both.
314    fn build_graph() -> (ModuleGraph, FxHashSet<FileId>) {
315        let files = vec![
316            file(0, "/p/index.js"),
317            file(1, "/p/src/impl.ts"),
318            file(2, "/p/src/internal.ts"),
319            file(3, "/p/src/consumer.ts"),
320        ];
321        let entry_points = vec![EntryPoint {
322            path: PathBuf::from("/p/index.js"),
323            source: EntryPointSource::PackageJsonExports,
324        }];
325        let resolved = vec![
326            ResolvedModule {
327                file_id: FileId(0),
328                path: PathBuf::from("/p/index.js"),
329                re_exports: vec![re_export("pub", "pub", FileId(1))],
330                ..Default::default()
331            },
332            ResolvedModule {
333                file_id: FileId(1),
334                path: PathBuf::from("/p/src/impl.ts"),
335                exports: vec![named_export("pub"), named_export("priv")].into(),
336                ..Default::default()
337            },
338            ResolvedModule {
339                file_id: FileId(2),
340                path: PathBuf::from("/p/src/internal.ts"),
341                re_exports: vec![re_export("priv", "priv", FileId(1))],
342                ..Default::default()
343            },
344            ResolvedModule {
345                file_id: FileId(3),
346                path: PathBuf::from("/p/src/consumer.ts"),
347                resolved_imports: vec![
348                    named_import("pub", FileId(0)),
349                    named_import("priv", FileId(2)),
350                ],
351                ..Default::default()
352            },
353        ];
354        let graph = ModuleGraph::build(&resolved, &entry_points, &files);
355        // The exports-mapped entry set: only index.js (PackageJsonExports).
356        let public_entries: FxHashSet<FileId> = std::iter::once(FileId(0)).collect();
357        (graph, public_entries)
358    }
359
360    #[test]
361    fn export_reexported_through_exports_path_is_public() {
362        let (graph, public_entries) = build_graph();
363        let keys = graph.public_export_keys(&public_entries, Path::new("/p"));
364        // `pub` is re-exported through the exports-mapped index.js, so it appears
365        // on the public surface keyed at the entry (the exposed name), not the
366        // internal origin.
367        assert!(
368            keys.contains("index.js::pub"),
369            "exports-reachable symbol must be public: {keys:?}"
370        );
371    }
372
373    #[test]
374    fn export_reexported_only_through_internal_barrel_is_not_public() {
375        let (graph, public_entries) = build_graph();
376        let keys = graph.public_export_keys(&public_entries, Path::new("/p"));
377        // `priv` reaches a consumer ONLY through the internal (non-exports)
378        // barrel, so it is on no public-surface key (neither the entry nor a
379        // star-target).
380        assert!(
381            !keys.iter().any(|k| k.ends_with("::priv")),
382            "internal-barrel-only symbol must NOT be public: {keys:?}"
383        );
384    }
385
386    /// Build the Aisha-repro graph parameterized by which impl symbols exist and
387    /// which is re-exported through the exports-mapped `index.js`. `internal`
388    /// (if present) is re-exported only through the non-exports internal barrel.
389    fn build_aisha_graph(
390        impl_exports: &[&str],
391        exports_reexported: &[&str],
392        internal_reexported: &[&str],
393    ) -> (ModuleGraph, FxHashSet<FileId>) {
394        let files = vec![
395            file(0, "/p/index.js"),
396            file(1, "/p/src/impl.ts"),
397            file(2, "/p/src/internal.ts"),
398        ];
399        let entry_points = vec![EntryPoint {
400            path: PathBuf::from("/p/index.js"),
401            source: EntryPointSource::PackageJsonExports,
402        }];
403        let resolved = vec![
404            ResolvedModule {
405                file_id: FileId(0),
406                path: PathBuf::from("/p/index.js"),
407                re_exports: exports_reexported
408                    .iter()
409                    .map(|n| re_export(n, n, FileId(1)))
410                    .collect(),
411                ..Default::default()
412            },
413            ResolvedModule {
414                file_id: FileId(1),
415                path: PathBuf::from("/p/src/impl.ts"),
416                exports: impl_exports.iter().map(|n| named_export(n)).collect(),
417                ..Default::default()
418            },
419            ResolvedModule {
420                file_id: FileId(2),
421                path: PathBuf::from("/p/src/internal.ts"),
422                re_exports: internal_reexported
423                    .iter()
424                    .map(|n| re_export(n, n, FileId(1)))
425                    .collect(),
426                ..Default::default()
427            },
428        ];
429        let graph = ModuleGraph::build(&resolved, &entry_points, &files);
430        let public_entries: FxHashSet<FileId> = std::iter::once(FileId(0)).collect();
431        (graph, public_entries)
432    }
433
434    #[test]
435    fn done_condition_internal_zero_exports_one() {
436        let root = Path::new("/p");
437        // Base: impl exports `pub`, re-exported through the exports-mapped index.
438        let (base_graph, base_entries) = build_aisha_graph(&["pub"], &["pub"], &[]);
439        let base = base_graph.public_export_keys(&base_entries, root);
440
441        // Head A: add an internal-barrel symbol NOT in exports -> 0 public deltas.
442        let (head_a_graph, head_a_entries) =
443            build_aisha_graph(&["pub", "internalOnly"], &["pub"], &["internalOnly"]);
444        let head_a = head_a_graph.public_export_keys(&head_a_entries, root);
445        let internal_delta: Vec<_> = head_a.difference(&base).collect();
446        assert!(
447            internal_delta.is_empty(),
448            "internal-barrel symbol must yield ZERO public-API delta: {internal_delta:?}"
449        );
450
451        // Head B: add a symbol reachable through the exports path -> exactly 1.
452        let (head_b_graph, head_b_entries) =
453            build_aisha_graph(&["pub", "widget"], &["pub", "widget"], &[]);
454        let head_b = head_b_graph.public_export_keys(&head_b_entries, root);
455        let exports_delta: Vec<_> = head_b.difference(&base).collect();
456        assert_eq!(
457            exports_delta.len(),
458            1,
459            "exports-reachable symbol must yield EXACTLY ONE public-API delta: {exports_delta:?}"
460        );
461        assert_eq!(exports_delta[0], "index.js::widget");
462    }
463
464    #[test]
465    fn type_only_exports_are_skipped() {
466        let files = vec![file(0, "/p/index.ts")];
467        let entry_points = vec![EntryPoint {
468            path: PathBuf::from("/p/index.ts"),
469            source: EntryPointSource::PackageJsonExports,
470        }];
471        let mut type_export = named_export("T");
472        type_export.is_type_only = true;
473        let resolved = vec![ResolvedModule {
474            file_id: FileId(0),
475            path: PathBuf::from("/p/index.ts"),
476            exports: vec![type_export, named_export("v")].into(),
477            ..Default::default()
478        }];
479        let graph = ModuleGraph::build(&resolved, &entry_points, &files);
480        let public_entries: FxHashSet<FileId> = std::iter::once(FileId(0)).collect();
481        let keys = graph.public_export_keys(&public_entries, Path::new("/p"));
482        assert!(keys.contains("index.ts::v"));
483        assert!(!keys.contains("index.ts::T"), "type-only export skipped");
484
485        let type_origins =
486            graph.public_export_origins_in_namespace(&public_entries, ExportNamespace::Type);
487        assert!(
488            type_origins
489                .iter()
490                .any(|origin| origin.export_name() == "T"),
491            "type namespace keeps the public declaration"
492        );
493        assert!(
494            graph
495                .public_export_origins(&public_entries)
496                .iter()
497                .all(|origin| origin.export_name() != "T"),
498            "value-only consumers still exclude type-only declarations"
499        );
500    }
501
502    #[test]
503    fn public_namespace_objects_expand_to_declaration_bindings() {
504        let (graph, public_entries) = build_star_surface_graph(
505            vec![re_export("*", "SDK", FileId(1))],
506            vec![ResolvedModule {
507                file_id: FileId(1),
508                path: PathBuf::from("/p/sdk.ts"),
509                exports: vec![named_export("Client")].into(),
510                ..Default::default()
511            }],
512        );
513
514        let declarations = graph.public_export_declaration_bindings(&public_entries);
515
516        assert_eq!(declarations.len(), 1);
517        let binding = *declarations.iter().next().expect("public Client binding");
518        let origin = graph.export_binding_origin(binding).expect("direct origin");
519        assert_eq!(origin.file_id(), FileId(1));
520        assert_eq!(origin.export().name.to_string(), "Client");
521    }
522
523    fn build_star_surface_graph(
524        entry_re_exports: Vec<ResolvedReExport>,
525        source_modules: Vec<ResolvedModule>,
526    ) -> (ModuleGraph, FxHashSet<FileId>) {
527        let mut files = vec![file(0, "/p/index.ts")];
528        files.extend(source_modules.iter().map(|module| {
529            file(
530                module.file_id.0,
531                module.path.to_str().expect("test path is UTF-8"),
532            )
533        }));
534        let entry_points = vec![EntryPoint {
535            path: PathBuf::from("/p/index.ts"),
536            source: EntryPointSource::PackageJsonExports,
537        }];
538        let mut resolved = vec![ResolvedModule {
539            file_id: FileId(0),
540            path: PathBuf::from("/p/index.ts"),
541            re_exports: entry_re_exports,
542            ..Default::default()
543        }];
544        resolved.extend(source_modules);
545        let graph = ModuleGraph::build(&resolved, &entry_points, &files);
546        (graph, std::iter::once(FileId(0)).collect())
547    }
548
549    #[test]
550    fn explicit_export_hides_shadowed_star_binding_from_public_surface() {
551        let (graph, public_entries) = build_star_surface_graph(
552            vec![
553                re_export("*", "*", FileId(1)),
554                re_export("foo", "foo", FileId(2)),
555            ],
556            vec![
557                ResolvedModule {
558                    file_id: FileId(1),
559                    path: PathBuf::from("/p/star-source.ts"),
560                    exports: vec![named_export("foo")].into(),
561                    ..Default::default()
562                },
563                ResolvedModule {
564                    file_id: FileId(2),
565                    path: PathBuf::from("/p/explicit-source.ts"),
566                    exports: vec![named_export("foo")].into(),
567                    ..Default::default()
568                },
569            ],
570        );
571
572        let keys = graph.public_export_keys(&public_entries, Path::new("/p"));
573
574        assert_eq!(keys, FxHashSet::from_iter(["index.ts::foo".to_string()]));
575    }
576
577    #[test]
578    fn ambiguous_star_binding_is_absent_from_public_surface() {
579        let (graph, public_entries) = build_star_surface_graph(
580            vec![
581                re_export("*", "*", FileId(1)),
582                re_export("*", "*", FileId(2)),
583            ],
584            vec![
585                ResolvedModule {
586                    file_id: FileId(1),
587                    path: PathBuf::from("/p/left.ts"),
588                    exports: vec![named_export("foo")].into(),
589                    ..Default::default()
590                },
591                ResolvedModule {
592                    file_id: FileId(2),
593                    path: PathBuf::from("/p/right.ts"),
594                    exports: vec![named_export("foo")].into(),
595                    ..Default::default()
596                },
597            ],
598        );
599
600        let keys = graph.public_export_keys(&public_entries, Path::new("/p"));
601
602        assert!(
603            keys.is_empty(),
604            "ambiguous foo is not a public binding: {keys:?}"
605        );
606    }
607
608    #[test]
609    fn convergent_star_diamond_has_one_exposed_public_key() {
610        let (graph, public_entries) = build_star_surface_graph(
611            vec![
612                re_export("*", "*", FileId(1)),
613                re_export("*", "*", FileId(2)),
614            ],
615            vec![
616                ResolvedModule {
617                    file_id: FileId(1),
618                    path: PathBuf::from("/p/left.ts"),
619                    re_exports: vec![re_export("*", "*", FileId(3))],
620                    ..Default::default()
621                },
622                ResolvedModule {
623                    file_id: FileId(2),
624                    path: PathBuf::from("/p/right.ts"),
625                    re_exports: vec![re_export("*", "*", FileId(3))],
626                    ..Default::default()
627                },
628                ResolvedModule {
629                    file_id: FileId(3),
630                    path: PathBuf::from("/p/source.ts"),
631                    exports: vec![named_export("foo")].into(),
632                    ..Default::default()
633                },
634            ],
635        );
636
637        let keys = graph.public_export_keys(&public_entries, Path::new("/p"));
638
639        assert_eq!(keys, FxHashSet::from_iter(["index.ts::foo".to_string()]));
640    }
641
642    #[test]
643    fn public_surface_preserves_aliases_default_and_value_namespace() {
644        let mut default_export = named_export("Widget");
645        default_export.name = ExportName::Default;
646        let mut type_export = named_export("T");
647        type_export.is_type_only = true;
648        let mut type_re_export = re_export("T", "PublicType", FileId(1));
649        type_re_export.info.is_type_only = true;
650        let (graph, public_entries) = build_star_surface_graph(
651            vec![
652                re_export("default", "default", FileId(1)),
653                re_export("default", "Widget", FileId(1)),
654                type_re_export,
655            ],
656            vec![ResolvedModule {
657                file_id: FileId(1),
658                path: PathBuf::from("/p/widget.ts"),
659                exports: vec![default_export, type_export].into(),
660                ..Default::default()
661            }],
662        );
663
664        let keys = graph.public_export_keys(&public_entries, Path::new("/p"));
665
666        assert_eq!(
667            keys,
668            FxHashSet::from_iter([
669                "index.ts::default".to_string(),
670                "index.ts::Widget".to_string(),
671            ])
672        );
673    }
674}