traverse_graph/
interface_resolver.rs

1/*
2    This module is designed to enhance static analysis of smart contracts by enabling
3    the resolution of abstract code elements (such as interfaces or uninitialized
4    library-type state variables) to concrete implementations specified by the user.
5
6    The core mechanism involves:
7    1. Defining a `BindingConfig` structure, which represents a concrete implementation
8       (e.g., a specific contract name, and optionally its address or chain ID).
9       These configurations are typically loaded from an external `binding.yaml` file.
10    2. A `BindingRegistry` loads and stores these `BindingConfig`s, indexed by a unique key.
11    3. An `InterfaceResolver` utilizes Natspec documentation comments extracted from
12       Solidity source code (via a `Manifest`). It specifically looks for
13       `@custom:binds-to <key>` tags within these comments.
14    4. When such a tag is found, the `InterfaceResolver` uses the `<key>` to query the
15       `BindingRegistry` for the corresponding `BindingConfig`. This effectively substitutes
16       the abstract element in the source code with the user-defined concrete
17       implementation for the purpose of the analysis.
18
19    By allowing users to specify these bindings, the static analysis can achieve
20    greater coverage and provide more accurate insights into the behavior of
21    complex smart contract systems where parts might be abstract or deployed separately.
22*/
23
24use crate::manifest::{Manifest, ManifestEntry}; // ManifestEntry is used as a parameter type, no need to import separately if manifest is the only one needed at top level
25use crate::natspec::{
26    extract::SourceItemKind, parse_natspec_comment, NatSpecKind, TextRange,
27};
28// NatSpec struct itself is not directly used in this file, only its kinds.
29use anyhow::{Context, Result};
30use serde::{Deserialize, Serialize};
31use std::{
32    collections::HashMap,
33    fs,
34    path::Path,
35};
36use tracing::debug;
37#[cfg(test)]
38use std::path::PathBuf;
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct BindingConfig {
42    pub key: String,
43    pub contract_name: Option<String>,
44    pub address: Option<String>,
45    pub chain_id: Option<u64>,
46    pub notes: Option<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
50pub struct BindingFile {
51    bindings: Vec<BindingConfig>,
52}
53
54impl BindingFile {
55    pub fn new(bindings: Vec<BindingConfig>) -> Self {
56        Self { bindings }
57    }
58}
59
60#[derive(Debug, Clone, Default)]
61pub struct BindingRegistry {
62    pub bindings: HashMap<String, BindingConfig>, // Made public for len() access in sol2cg
63}
64
65impl BindingRegistry {
66    pub fn load(path: &Path) -> Result<Self> {
67        let file_content = fs::read_to_string(path)
68            .with_context(|| format!("Failed to read binding file: {}", path.display()))?;
69        let binding_file: BindingFile = serde_yaml::from_str(&file_content).with_context(|| {
70            format!(
71                "Failed to deserialize binding file from YAML: {}",
72                path.display()
73            )
74        })?;
75
76        let mut bindings_map = HashMap::new();
77        for config in binding_file.bindings {
78            if bindings_map.contains_key(&config.key) {
79                debug!(
80                    "Warning: Duplicate binding key '{}' found in {}. The last definition will be used.",
81                    config.key,
82                    path.display()
83                );
84            }
85            bindings_map.insert(config.key.clone(), config);
86        }
87
88        Ok(BindingRegistry {
89            bindings: bindings_map,
90        })
91    }
92
93    pub fn get_binding(&self, key: &str) -> Option<&BindingConfig> {
94        self.bindings.get(key)
95    }
96
97    /// Populates the registry with bindings derived from Natspec comments in the manifest,
98    /// particularly `@custom:binds-to` tags on concrete contracts.
99    pub fn populate_from_manifest(&mut self, manifest: &Manifest) {
100        for entry in &manifest.entries {
101            if !entry.is_natspec || entry.item_kind != SourceItemKind::Contract {
102                continue;
103            }
104
105            if let Ok(natspec) = parse_natspec_comment(&entry.text) {
106                for item in natspec.items {
107                    if let NatSpecKind::Custom { tag } = item.kind {
108                        if tag == "binds-to" {
109                            let binding_key = item.comment.trim();
110                            if binding_key.is_empty() {
111                                debug!(
112                                    "Warning: Found '@custom:binds-to' with empty key in Natspec for item {:?} in file {}",
113                                    entry.item_name, entry.file_path.display()
114                                );
115                                continue;
116                            }
117
118                            if let Some(contract_name_val) = &entry.item_name {
119                                let config = self
120                                    .bindings
121                                    .entry(binding_key.to_string())
122                                    .or_insert_with(|| {
123                                        debug!(
124                                            "Natspec: Creating new binding for key '{}' from contract '{}'",
125                                            binding_key, contract_name_val
126                                        );
127                                        BindingConfig {
128                                            key: binding_key.to_string(),
129                                            contract_name: None, // Will be set below
130                                            address: None,
131                                            chain_id: None,
132                                            notes: Some(format!(
133                                                "Binding key '{}' initially found on contract '{}' via Natspec.",
134                                                binding_key, contract_name_val
135                                            )),
136                                        }
137                                    });
138
139                                if config.contract_name.is_some()
140                                    && config.contract_name.as_deref() != Some(contract_name_val)
141                                {
142                                    debug!(
143                                        "Warning: Overwriting Natspec-derived binding for key '{}'. \
144                                        Previous contract_name: {:?}, New (from contract '{}'): '{}'. \
145                                        File: {}",
146                                        binding_key,
147                                        config.contract_name,
148                                        contract_name_val,
149                                        contract_name_val,
150                                        entry.file_path.display()
151                                    );
152                                } else if config.contract_name.is_none() {
153                                     debug!(
154                                        "Natspec: Setting contract_name for key '{}' to '{}' from contract '{}'",
155                                        binding_key, contract_name_val, contract_name_val
156                                    );
157                                }
158
159
160                                config.contract_name = Some(contract_name_val.clone());
161                                // Enhance notes
162                                config.notes = Some(format!(
163                                    "Binding key '{}' concretely implemented by contract '{}' (derived from Natspec on the contract itself in {}).{}",
164                                    binding_key,
165                                    contract_name_val,
166                                    entry.file_path.display(),
167                                    config.notes.as_ref().map_or("".to_string(), |n| format!(" Previous notes: {}", n))
168                                ));
169                            }
170                            break; // Assuming one binds-to per Natspec block is primary
171                        }
172                    }
173                }
174            }
175        }
176    }
177}
178
179pub struct InterfaceResolver<'m, 'r> {
180    manifest: &'m Manifest,
181    registry: &'r BindingRegistry,
182}
183
184impl<'m, 'r> InterfaceResolver<'m, 'r> {
185    pub fn new(manifest: &'m Manifest, registry: &'r BindingRegistry) -> Self {
186        Self { manifest, registry }
187    }
188
189    pub fn resolve_for_entry(&self, entry: &ManifestEntry) -> Result<Option<&'r BindingConfig>> {
190        if !entry.is_natspec {
191            return Ok(None);
192        }
193
194        match parse_natspec_comment(&entry.text) {
195            Ok(natspec) => {
196                for item in natspec.items {
197                    if let NatSpecKind::Custom { tag } = item.kind {
198                        if tag == "binds-to" {
199                            let binding_key = item.comment.trim();
200                            if !binding_key.is_empty() {
201                                return Ok(self.registry.get_binding(binding_key));
202                            } else {
203                                debug!(
204                                    "Warning: Found '@custom:binds-to' with empty key in Natspec for item {:?} in file {}",
205                                    entry.item_name, entry.file_path.display()
206                                );
207                            }
208                        }
209                    }
210                }
211                Ok(None)
212            }
213            Err(e) => Err(anyhow::anyhow!(
214                "Failed to parse Natspec for item {:?} (file: {}): {}",
215                entry.item_name.as_deref().unwrap_or("<unknown>"),
216                entry.file_path.display(),
217                e.to_string()
218            )),
219        }
220    }
221
222    pub fn resolve_for_item_span(
223        &self,
224        target_item_span: &TextRange,
225    ) -> Result<Option<&'r BindingConfig>> {
226        for entry in &self.manifest.entries {
227            if entry.item_span == *target_item_span {
228                return self.resolve_for_entry(entry);
229            }
230        }
231        Ok(None)
232    }
233
234    pub fn resolve_for_item_name_kind(
235        &self,
236        item_name_to_find: &str,
237        item_kind_to_find: SourceItemKind,
238        file_path_hint: Option<&Path>,
239    ) -> Result<Option<&'r BindingConfig>> {
240        for entry in &self.manifest.entries {
241            if entry.item_kind == item_kind_to_find
242                && entry.item_name.as_deref() == Some(item_name_to_find)
243            {
244                if let Some(hint_path) = file_path_hint {
245                    if entry.file_path != hint_path {
246                        continue;
247                    }
248                }
249                return self.resolve_for_entry(entry);
250            }
251        }
252        Ok(None)
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::manifest::ManifestEntry; // Added for tests
260    use crate::natspec::extract::SourceItemKind;
261    use crate::natspec::{TextIndex, TextRange};
262    use std::fs::File;
263    use std::io::Write;
264    use tempfile::tempdir;
265
266    fn create_binding_file(dir: &Path, content: &str) -> PathBuf {
267        let file_path = dir.join("binding.yaml");
268        let mut file = File::create(&file_path).unwrap();
269        writeln!(file, "{}", content).unwrap();
270        file_path
271    }
272
273    fn default_text_range() -> TextRange {
274        TextRange {
275            start: TextIndex {
276                utf8: 0,
277                line: 0,
278                column: 0,
279            },
280            end: TextIndex {
281                utf8: 0,
282                line: 0,
283                column: 0,
284            },
285        }
286    }
287
288    #[test]
289    fn test_load_binding_registry() -> Result<()> {
290        let tmp_dir = tempdir()?;
291        let yaml_content = r#"
292bindings:
293  - key: "USDC_Mainnet"
294    contract_name: "FiatTokenProxy"
295    address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
296    chain_id: 1
297    notes: "USDC on Ethereum Mainnet"
298  - key: "MyService_Test"
299    contract_name: "MyServiceImpl"
300    address: "0x1234567890abcdef1234567890abcdef12345678"
301    chain_id: 4
302    notes: "MyService on Rinkeby (test)"
303"#;
304        let binding_file_path = create_binding_file(tmp_dir.path(), yaml_content);
305        let registry = BindingRegistry::load(&binding_file_path)?;
306
307        assert!(registry.get_binding("USDC_Mainnet").is_some());
308        assert_eq!(
309            registry.get_binding("USDC_Mainnet").unwrap().address,
310            Some("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string())
311        );
312        assert_eq!(
313            registry.get_binding("USDC_Mainnet").unwrap().chain_id,
314            Some(1)
315        );
316
317        assert!(registry.get_binding("MyService_Test").is_some());
318        assert_eq!(
319            registry
320                .get_binding("MyService_Test")
321                .unwrap()
322                .contract_name,
323            Some("MyServiceImpl".to_string())
324        );
325
326        assert!(registry.get_binding("NonExistentKey").is_none());
327        Ok(())
328    }
329
330    #[test]
331    fn test_load_binding_registry_duplicate_keys() -> Result<()> {
332        let tmp_dir = tempdir()?;
333        let yaml_content = r#"
334bindings:
335  - key: "DuplicateKey"
336    address: "0x111"
337  - key: "UniqueKey"
338    address: "0x222"
339  - key: "DuplicateKey" # This should overwrite the first one
340    address: "0x333"
341    notes: "This is the one"
342"#;
343        let binding_file_path = create_binding_file(tmp_dir.path(), yaml_content);
344        let registry = BindingRegistry::load(&binding_file_path)?;
345
346        assert_eq!(registry.bindings.len(), 2); // DuplicateKey and UniqueKey
347        let duplicate_entry = registry.get_binding("DuplicateKey").unwrap();
348        assert_eq!(duplicate_entry.address, Some("0x333".to_string()));
349        assert_eq!(duplicate_entry.notes, Some("This is the one".to_string()));
350        Ok(())
351    }
352
353    #[test]
354    fn test_resolve_for_entry_success() -> Result<()> {
355        let registry = BindingRegistry {
356            bindings: HashMap::from([(
357                "BoundContract_Key".to_string(),
358                BindingConfig {
359                    key: "BoundContract_Key".to_string(),
360                    contract_name: Some("BoundContractImpl".to_string()),
361                    address: Some("0xabc".to_string()),
362                    chain_id: Some(1),
363                    notes: None,
364                },
365            )]),
366        };
367        let manifest = Manifest::default(); // Not used directly by this specific path of resolve_for_entry
368
369        let entry = ManifestEntry {
370            file_path: PathBuf::from("test.sol"),
371            text: "/// @custom:binds-to BoundContract_Key".to_string(),
372            raw_comment_span: default_text_range(),
373            item_kind: SourceItemKind::Contract,
374            item_name: Some("MyContract".to_string()),
375            item_span: default_text_range(),
376            is_natspec: true,
377        };
378
379        let resolver = InterfaceResolver::new(&manifest, &registry);
380        let binding_config = resolver.resolve_for_entry(&entry)?.unwrap();
381
382        assert_eq!(binding_config.key, "BoundContract_Key");
383        assert_eq!(
384            binding_config.contract_name,
385            Some("BoundContractImpl".to_string())
386        );
387        Ok(())
388    }
389
390    #[test]
391    fn test_resolve_for_entry_multiline_natspec() -> Result<()> {
392        let registry = BindingRegistry {
393            bindings: HashMap::from([(
394                "MultiKey".to_string(),
395                BindingConfig {
396                    key: "MultiKey".to_string(),
397                    contract_name: Some("MultiImpl".to_string()),
398                    address: None,
399                    chain_id: None,
400                    notes: None,
401                },
402            )]),
403        };
404        let manifest = Manifest::default();
405        let entry = ManifestEntry {
406            file_path: PathBuf::from("test.sol"),
407            text: "/**\n * @notice Some notice\n * @custom:binds-to MultiKey\n * @dev Some dev comment\n */".to_string(),
408            raw_comment_span: default_text_range(),
409            item_kind: SourceItemKind::Function,
410            item_name: Some("doSomething".to_string()),
411            item_span: default_text_range(),
412            is_natspec: true,
413        };
414        let resolver = InterfaceResolver::new(&manifest, &registry);
415        let binding_config = resolver.resolve_for_entry(&entry)?.unwrap();
416        assert_eq!(binding_config.key, "MultiKey");
417        Ok(())
418    }
419
420    #[test]
421    fn test_resolve_for_entry_no_binding_tag() -> Result<()> {
422        let registry = BindingRegistry::default();
423        let manifest = Manifest::default();
424        let entry = ManifestEntry {
425            file_path: PathBuf::from("test.sol"),
426            text: "/// @notice Just a regular comment".to_string(),
427            raw_comment_span: default_text_range(),
428            item_kind: SourceItemKind::Contract,
429            item_name: Some("MyContract".to_string()),
430            item_span: default_text_range(),
431            is_natspec: true,
432        };
433
434        let resolver = InterfaceResolver::new(&manifest, &registry);
435        assert!(resolver.resolve_for_entry(&entry)?.is_none());
436        Ok(())
437    }
438
439    #[test]
440    fn test_resolve_for_entry_not_natspec() -> Result<()> {
441        let registry = BindingRegistry::default();
442        let manifest = Manifest::default();
443        let entry = ManifestEntry {
444            file_path: PathBuf::from("test.sol"),
445            text: "// Regular comment, @custom:binds-to SomeKey".to_string(),
446            raw_comment_span: default_text_range(),
447            item_kind: SourceItemKind::Contract,
448            item_name: Some("MyContract".to_string()),
449            item_span: default_text_range(),
450            is_natspec: false, // Crucial part
451        };
452
453        let resolver = InterfaceResolver::new(&manifest, &registry);
454        assert!(resolver.resolve_for_entry(&entry)?.is_none());
455        Ok(())
456    }
457
458    #[test]
459    fn test_resolve_for_entry_key_not_in_registry() -> Result<()> {
460        let registry = BindingRegistry::default(); // Empty registry
461        let manifest = Manifest::default();
462        let entry = ManifestEntry {
463            file_path: PathBuf::from("test.sol"),
464            text: "/// @custom:binds-to NonExistentKey".to_string(),
465            raw_comment_span: default_text_range(),
466            item_kind: SourceItemKind::Contract,
467            item_name: Some("MyContract".to_string()),
468            item_span: default_text_range(),
469            is_natspec: true,
470        };
471
472        let resolver = InterfaceResolver::new(&manifest, &registry);
473        assert!(resolver.resolve_for_entry(&entry)?.is_none());
474        Ok(())
475    }
476
477    #[test]
478    fn test_resolve_for_entry_natspec_parse_error() {
479        let registry = BindingRegistry::default();
480        let manifest = Manifest::default();
481        let entry = ManifestEntry {
482            file_path: PathBuf::from("test.sol"),
483            text: "/*** Invalid Natspec @custom:binds-to SomeKey".to_string(), // Invalid start
484            raw_comment_span: default_text_range(),
485            item_kind: SourceItemKind::Contract,
486            item_name: Some("MyContract".to_string()),
487            item_span: default_text_range(),
488            is_natspec: true,
489        };
490        let resolver = InterfaceResolver::new(&manifest, &registry);
491        let result = resolver.resolve_for_entry(&entry);
492        assert!(result.is_err());
493        assert!(result
494            .unwrap_err()
495            .to_string()
496            .contains("Failed to parse Natspec"));
497    }
498
499    #[test]
500    fn test_resolve_for_item_span_found() -> Result<()> {
501        let registry = BindingRegistry {
502            bindings: HashMap::from([(
503                "SpanKey".to_string(),
504                BindingConfig {
505                    key: "SpanKey".to_string(),
506                    address: Some("0xspan".to_string()),
507                    contract_name: None,
508                    chain_id: None,
509                    notes: None,
510                },
511            )]),
512        };
513        let item_span_to_find = TextRange {
514            start: TextIndex {
515                utf8: 10,
516                line: 1,
517                column: 0,
518            },
519            end: TextIndex {
520                utf8: 20,
521                line: 1,
522                column: 10,
523            },
524        };
525        let mut manifest = Manifest::default();
526        manifest.add_entry(ManifestEntry {
527            file_path: PathBuf::from("a.sol"),
528            text: "/// @custom:binds-to SpanKey".to_string(),
529            raw_comment_span: default_text_range(),
530            item_kind: SourceItemKind::Function,
531            item_name: Some("funcA".to_string()),
532            item_span: item_span_to_find.clone(),
533            is_natspec: true,
534        });
535        manifest.add_entry(ManifestEntry {
536            // Another entry with different span
537            file_path: PathBuf::from("b.sol"),
538            text: "/// @custom:binds-to OtherKey".to_string(),
539            raw_comment_span: default_text_range(),
540            item_kind: SourceItemKind::Contract,
541            item_name: Some("ContractB".to_string()),
542            item_span: default_text_range(),
543            is_natspec: true,
544        });
545
546        let resolver = InterfaceResolver::new(&manifest, &registry);
547        let binding = resolver.resolve_for_item_span(&item_span_to_find)?.unwrap();
548        assert_eq!(binding.key, "SpanKey");
549        assert_eq!(binding.address, Some("0xspan".to_string()));
550        Ok(())
551    }
552
553    #[test]
554    fn test_resolve_for_item_name_kind_found() -> Result<()> {
555        let registry = BindingRegistry {
556            bindings: HashMap::from([(
557                "NameKindKey".to_string(),
558                BindingConfig {
559                    key: "NameKindKey".to_string(),
560                    contract_name: Some("ImplForName".to_string()),
561                    address: None,
562                    chain_id: None,
563                    notes: None,
564                },
565            )]),
566        };
567        let mut manifest = Manifest::default();
568        manifest.add_entry(ManifestEntry {
569            file_path: PathBuf::from("c.sol"),
570            text: "/// @custom:binds-to NameKindKey".to_string(),
571            raw_comment_span: default_text_range(),
572            item_kind: SourceItemKind::Interface,
573            item_name: Some("IMyInterface".to_string()),
574            item_span: default_text_range(),
575            is_natspec: true,
576        });
577
578        let resolver = InterfaceResolver::new(&manifest, &registry);
579        let binding = resolver
580            .resolve_for_item_name_kind("IMyInterface", SourceItemKind::Interface, None)?
581            .unwrap();
582        assert_eq!(binding.key, "NameKindKey");
583        assert_eq!(binding.contract_name, Some("ImplForName".to_string()));
584
585        // Test with file path hint
586        let binding_with_hint = resolver
587            .resolve_for_item_name_kind(
588                "IMyInterface",
589                SourceItemKind::Interface,
590                Some(&PathBuf::from("c.sol")),
591            )?
592            .unwrap();
593        assert_eq!(binding_with_hint.key, "NameKindKey");
594
595        // Test with wrong file path hint
596        let no_binding_wrong_hint = resolver.resolve_for_item_name_kind(
597            "IMyInterface",
598            SourceItemKind::Interface,
599            Some(&PathBuf::from("wrong.sol")),
600        )?;
601        assert!(no_binding_wrong_hint.is_none());
602        Ok(())
603    }
604
605    #[test]
606    fn test_populate_from_manifest_new_and_update() {
607        let mut registry = BindingRegistry::default();
608        let mut manifest = Manifest::default();
609
610        // Entry 1: New binding from a contract's Natspec
611        manifest.add_entry(ManifestEntry {
612            file_path: PathBuf::from("contracts/ConcreteA.sol"),
613            text: "/// @custom:binds-to KeyA".to_string(),
614            raw_comment_span: default_text_range(),
615            item_kind: SourceItemKind::Contract,
616            item_name: Some("ConcreteA".to_string()),
617            item_span: default_text_range(),
618            is_natspec: true,
619        });
620
621        // Entry 2: Another contract binding to a different key
622        manifest.add_entry(ManifestEntry {
623            file_path: PathBuf::from("contracts/ConcreteB.sol"),
624            text: "/** @custom:binds-to KeyB */".to_string(),
625            raw_comment_span: default_text_range(),
626            item_kind: SourceItemKind::Contract,
627            item_name: Some("ConcreteB".to_string()),
628            item_span: default_text_range(),
629            is_natspec: true,
630        });
631
632        // Entry 3: A contract that will update an existing binding (e.g., if KeyA was already known from an interface)
633        // Let's pre-populate KeyA in the registry as if it came from an interface Natspec (no concrete contract yet)
634        registry.bindings.insert(
635            "KeyA".to_string(),
636            BindingConfig {
637                key: "KeyA".to_string(),
638                contract_name: None, // No concrete contract known yet
639                address: None,
640                chain_id: None,
641                notes: Some("Initially from IKeyA interface Natspec".to_string()),
642            },
643        );
644        // Now, ConcreteC also binds to KeyA, which should fill in the contract_name
645        manifest.add_entry(ManifestEntry {
646            file_path: PathBuf::from("contracts/ConcreteC.sol"),
647            text: "/// @custom:binds-to KeyA".to_string(), // Same key as ConcreteA, but we'll process ConcreteA first
648            raw_comment_span: default_text_range(),
649            item_kind: SourceItemKind::Contract,
650            item_name: Some("ConcreteC".to_string()), // This should ideally be the one that "wins" if processed later, or cause a warning
651            item_span: default_text_range(),
652            is_natspec: true,
653        });
654
655
656        // Entry 4: Not a contract, should be ignored
657        manifest.add_entry(ManifestEntry {
658            file_path: PathBuf::from("interfaces/IfaceD.sol"),
659            text: "/// @custom:binds-to KeyD".to_string(),
660            raw_comment_span: default_text_range(),
661            item_kind: SourceItemKind::Interface, // Not a contract
662            item_name: Some("IfaceD".to_string()),
663            item_span: default_text_range(),
664            is_natspec: true,
665        });
666
667        // Entry 5: Contract, but not Natspec, should be ignored
668        manifest.add_entry(ManifestEntry {
669            file_path: PathBuf::from("contracts/ConcreteE.sol"),
670            text: "// @custom:binds-to KeyE".to_string(),
671            raw_comment_span: default_text_range(),
672            item_kind: SourceItemKind::Contract,
673            item_name: Some("ConcreteE".to_string()),
674            item_span: default_text_range(),
675            is_natspec: false, // Not Natspec
676        });
677
678
679        registry.populate_from_manifest(&manifest);
680
681        assert_eq!(registry.bindings.len(), 2); // KeyA, KeyB. KeyD and KeyE should not be added.
682
683        // Check KeyA: It was pre-populated, then ConcreteA bound to it, then ConcreteC.
684        // The manifest processing order is ConcreteA then ConcreteC for KeyA.
685        // So ConcreteC should be the final contract_name.
686        let binding_a = registry.get_binding("KeyA").unwrap();
687        assert_eq!(binding_a.contract_name, Some("ConcreteC".to_string()));
688        assert!(binding_a.notes.as_ref().unwrap().contains("ConcreteC"));
689        assert!(binding_a.notes.as_ref().unwrap().contains("Initially from IKeyA interface Natspec"));
690
691
692        // Check KeyB
693        let binding_b = registry.get_binding("KeyB").unwrap();
694        assert_eq!(binding_b.contract_name, Some("ConcreteB".to_string()));
695        assert!(binding_b.notes.as_ref().unwrap().contains("ConcreteB"));
696
697        // Ensure KeyD and KeyE were not processed
698        assert!(registry.get_binding("KeyD").is_none());
699        assert!(registry.get_binding("KeyE").is_none());
700    }
701}