Skip to main content

solidity_language_server/
types.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Type wrapper for AST node IDs.
5///
6/// Every node in the Solidity compiler's JSON AST has a unique numeric `id`.
7/// Wrapping it prevents accidental mixups with [`FileId`] or plain integers.
8///
9/// Signed because solc uses negative IDs for built-in symbols (e.g. `-1` for
10/// `abi`, `-15` for `msg`, `-18` for `require`, `-28` for `this`).
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12pub struct NodeId(pub i64);
13
14/// Newtype wrapper for source file IDs.
15///
16/// The compiler assigns each input file a numeric ID that appears in `src`
17/// strings (`"offset:length:fileId"`). Wrapping it prevents accidental
18/// mixups with [`NodeId`] or plain integers.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
20pub struct FileId(pub u64);
21
22/// Type wrapper for Solidity compiler diagnostic error codes.
23///
24/// Solc and forge diagnostics carry numeric codes like `2072`, `1878`, and
25/// `7359`. Wrapping the integer avoids mixing error-code keys with unrelated
26/// numeric IDs in maps.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
28#[serde(transparent)]
29pub struct ErrorCode(pub u32);
30
31/// A parsed `"offset:length:fileId"` source location from the AST.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct SourceLoc {
34    /// Byte offset in the source file.
35    pub offset: usize,
36    /// Byte length of the source range.
37    pub length: usize,
38    /// ID of the source file this location belongs to.
39    pub file_id: FileId,
40}
41
42impl SourceLoc {
43    /// Parse a `"offset:length:fileId"` string.
44    ///
45    /// Returns `None` if the format is invalid or any component fails to parse.
46    pub fn parse(src: &str) -> Option<Self> {
47        let mut parts = src.split(':');
48        let offset = parts.next()?.parse::<usize>().ok()?;
49        let length = parts.next()?.parse::<usize>().ok()?;
50        let file_id = parts.next()?.parse::<u64>().ok()?;
51        // Reject if there are extra parts
52        if parts.next().is_some() {
53            return None;
54        }
55        Some(Self {
56            offset,
57            length,
58            file_id: FileId(file_id),
59        })
60    }
61
62    /// End byte offset (`offset + length`).
63    pub fn end(&self) -> usize {
64        self.offset + self.length
65    }
66
67    /// The file ID as a [`SolcFileId`], for use as a HashMap key when
68    /// interacting with the `id_to_path_map`.
69    pub fn file_id_str(&self) -> SolcFileId {
70        SolcFileId::new(self.file_id.0.to_string())
71    }
72}
73
74impl std::fmt::Display for NodeId {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "{}", self.0)
77    }
78}
79
80impl std::fmt::Display for FileId {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        write!(f, "{}", self.0)
83    }
84}
85
86impl std::fmt::Display for ErrorCode {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        write!(f, "{}", self.0)
89    }
90}
91
92impl From<u32> for ErrorCode {
93    fn from(value: u32) -> Self {
94        Self(value)
95    }
96}
97
98impl std::borrow::Borrow<u32> for ErrorCode {
99    fn borrow(&self) -> &u32 {
100        &self.0
101    }
102}
103
104// ── Typed string wrappers ──────────────────────────────────────────────────
105//
106// These newtypes replace bare `String` keys in HashMaps so readers can
107// instantly tell what a value represents. They serialize as plain strings
108// for JSON cache backwards-compatibility.
109
110/// An absolute file path from the AST (`absolutePath` field) or filesystem.
111///
112/// Used as the key in [`CachedBuild::nodes`], values in
113/// [`CachedBuild::path_to_abs`], and keys in [`HintIndex`].
114///
115/// # Examples
116/// ```ignore
117/// AbsPath::new("src/PoolManager.sol")            // AST absolutePath
118/// AbsPath::new("/Users/me/project/src/Foo.sol")   // filesystem abs path
119/// ```
120#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
121#[serde(transparent)]
122pub struct AbsPath(String);
123
124impl AbsPath {
125    pub fn new(s: impl Into<String>) -> Self {
126        Self(s.into())
127    }
128    pub fn as_str(&self) -> &str {
129        &self.0
130    }
131    /// Consume and return the inner `String`.
132    pub fn into_inner(self) -> String {
133        self.0
134    }
135}
136
137impl std::fmt::Display for AbsPath {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        f.write_str(&self.0)
140    }
141}
142
143impl AsRef<str> for AbsPath {
144    fn as_ref(&self) -> &str {
145        &self.0
146    }
147}
148
149impl std::ops::Deref for AbsPath {
150    type Target = str;
151    fn deref(&self) -> &Self::Target {
152        &self.0
153    }
154}
155
156impl AsRef<std::path::Path> for AbsPath {
157    fn as_ref(&self) -> &std::path::Path {
158        std::path::Path::new(&self.0)
159    }
160}
161
162impl From<String> for AbsPath {
163    fn from(s: String) -> Self {
164        Self(s)
165    }
166}
167
168impl From<&str> for AbsPath {
169    fn from(s: &str) -> Self {
170        Self(s.to_owned())
171    }
172}
173
174impl std::borrow::Borrow<str> for AbsPath {
175    fn borrow(&self) -> &str {
176        &self.0
177    }
178}
179
180/// A project-relative Solidity source path (e.g. `src/Foo.sol`, `lib/X.sol`).
181///
182/// This wraps keys used in maps like `path_to_abs`, where solc emits paths
183/// relative to the project root.
184#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
185#[serde(transparent)]
186pub struct RelPath(String);
187
188impl RelPath {
189    pub fn new(s: impl Into<String>) -> Self {
190        Self(s.into())
191    }
192    pub fn as_str(&self) -> &str {
193        &self.0
194    }
195    pub fn into_inner(self) -> String {
196        self.0
197    }
198}
199
200impl std::fmt::Display for RelPath {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        f.write_str(&self.0)
203    }
204}
205
206impl AsRef<str> for RelPath {
207    fn as_ref(&self) -> &str {
208        &self.0
209    }
210}
211
212impl std::ops::Deref for RelPath {
213    type Target = str;
214    fn deref(&self) -> &Self::Target {
215        &self.0
216    }
217}
218
219impl From<String> for RelPath {
220    fn from(s: String) -> Self {
221        Self(s)
222    }
223}
224
225impl From<&str> for RelPath {
226    fn from(s: &str) -> Self {
227        Self(s.to_owned())
228    }
229}
230
231impl std::borrow::Borrow<str> for RelPath {
232    fn borrow(&self) -> &str {
233        &self.0
234    }
235}
236
237/// Identifier/symbol name used for completion indexes.
238#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
239#[serde(transparent)]
240pub struct SymbolName(String);
241
242impl SymbolName {
243    pub fn new(s: impl Into<String>) -> Self {
244        Self(s.into())
245    }
246    pub fn as_str(&self) -> &str {
247        &self.0
248    }
249}
250
251impl std::ops::Deref for SymbolName {
252    type Target = str;
253    fn deref(&self) -> &Self::Target {
254        &self.0
255    }
256}
257
258impl std::fmt::Display for SymbolName {
259    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260        f.write_str(&self.0)
261    }
262}
263
264impl From<String> for SymbolName {
265    fn from(s: String) -> Self {
266        Self(s)
267    }
268}
269
270impl From<&str> for SymbolName {
271    fn from(s: &str) -> Self {
272        Self(s.to_owned())
273    }
274}
275
276impl std::borrow::Borrow<str> for SymbolName {
277    fn borrow(&self) -> &str {
278        &self.0
279    }
280}
281
282impl std::borrow::Borrow<String> for SymbolName {
283    fn borrow(&self) -> &String {
284        &self.0
285    }
286}
287
288/// Solidity `typeIdentifier` string used in completion resolution.
289#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
290#[serde(transparent)]
291pub struct TypeIdentifier(String);
292
293impl TypeIdentifier {
294    pub fn new(s: impl Into<String>) -> Self {
295        Self(s.into())
296    }
297    pub fn as_str(&self) -> &str {
298        &self.0
299    }
300}
301
302impl std::ops::Deref for TypeIdentifier {
303    type Target = str;
304    fn deref(&self) -> &Self::Target {
305        &self.0
306    }
307}
308
309impl std::fmt::Display for TypeIdentifier {
310    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311        f.write_str(&self.0)
312    }
313}
314
315impl From<String> for TypeIdentifier {
316    fn from(s: String) -> Self {
317        Self(s)
318    }
319}
320
321impl From<&str> for TypeIdentifier {
322    fn from(s: &str) -> Self {
323        Self(s.to_owned())
324    }
325}
326
327impl std::borrow::Borrow<str> for TypeIdentifier {
328    fn borrow(&self) -> &str {
329        &self.0
330    }
331}
332
333impl std::borrow::Borrow<String> for TypeIdentifier {
334    fn borrow(&self) -> &String {
335        &self.0
336    }
337}
338
339/// A solc source-file ID in string form (e.g. `"0"`, `"34"`, `"127"`).
340///
341/// The compiler assigns each input file a numeric ID that appears as the
342/// third component of `src` strings (`"offset:length:fileId"`). This
343/// newtype wraps the stringified form used as keys in
344/// [`CachedBuild::id_to_path_map`] and the `id_remap` table during
345/// incremental merges.
346#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
347#[serde(transparent)]
348pub struct SolcFileId(String);
349
350impl SolcFileId {
351    pub fn new(s: impl Into<String>) -> Self {
352        Self(s.into())
353    }
354    pub fn as_str(&self) -> &str {
355        &self.0
356    }
357    pub fn into_inner(self) -> String {
358        self.0
359    }
360}
361
362impl std::fmt::Display for SolcFileId {
363    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364        f.write_str(&self.0)
365    }
366}
367
368impl AsRef<str> for SolcFileId {
369    fn as_ref(&self) -> &str {
370        &self.0
371    }
372}
373
374impl std::ops::Deref for SolcFileId {
375    type Target = str;
376    fn deref(&self) -> &Self::Target {
377        &self.0
378    }
379}
380
381impl From<String> for SolcFileId {
382    fn from(s: String) -> Self {
383        Self(s)
384    }
385}
386
387impl From<&str> for SolcFileId {
388    fn from(s: &str) -> Self {
389        Self(s.to_owned())
390    }
391}
392
393impl std::borrow::Borrow<str> for SolcFileId {
394    fn borrow(&self) -> &str {
395        &self.0
396    }
397}
398
399/// A raw `"offset:length:fileId"` source-location string from the AST.
400///
401/// This is the **unparsed** form stored on [`NodeInfo::src`] and used as
402/// keys in [`ExternalRefs`]. For the parsed representation with typed
403/// fields, see [`SourceLoc::parse`].
404///
405/// # Examples
406/// ```ignore
407/// SrcLocation::new("2068:10:33")
408/// ```
409#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
410#[serde(transparent)]
411pub struct SrcLocation(String);
412
413impl SrcLocation {
414    pub fn new(s: impl Into<String>) -> Self {
415        Self(s.into())
416    }
417    pub fn as_str(&self) -> &str {
418        &self.0
419    }
420    pub fn into_inner(self) -> String {
421        self.0
422    }
423    /// Parse into a structured [`SourceLoc`].
424    pub fn parse(&self) -> Option<SourceLoc> {
425        SourceLoc::parse(&self.0)
426    }
427}
428
429impl std::fmt::Display for SrcLocation {
430    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431        f.write_str(&self.0)
432    }
433}
434
435impl AsRef<str> for SrcLocation {
436    fn as_ref(&self) -> &str {
437        &self.0
438    }
439}
440
441impl std::ops::Deref for SrcLocation {
442    type Target = str;
443    fn deref(&self) -> &Self::Target {
444        &self.0
445    }
446}
447
448impl PartialEq<&str> for SrcLocation {
449    fn eq(&self, other: &&str) -> bool {
450        self.0 == *other
451    }
452}
453
454impl PartialEq<SrcLocation> for &str {
455    fn eq(&self, other: &SrcLocation) -> bool {
456        *self == other.0
457    }
458}
459
460impl From<String> for SrcLocation {
461    fn from(s: String) -> Self {
462        Self(s)
463    }
464}
465
466impl From<&str> for SrcLocation {
467    fn from(s: &str) -> Self {
468        Self(s.to_owned())
469    }
470}
471
472impl std::borrow::Borrow<str> for SrcLocation {
473    fn borrow(&self) -> &str {
474        &self.0
475    }
476}
477
478/// An LSP document URI string (e.g. `"file:///Users/me/project/src/Foo.sol"`).
479///
480/// Used as keys in [`ForgeLsp::ast_cache`], [`ForgeLsp::text_cache`],
481/// [`ForgeLsp::completion_cache`], [`SemanticTokenCache`], and
482/// [`ForgeLsp::did_save_workers`].
483#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
484#[serde(transparent)]
485pub struct DocumentUri(String);
486
487impl DocumentUri {
488    pub fn new(s: impl Into<String>) -> Self {
489        Self(s.into())
490    }
491    pub fn as_str(&self) -> &str {
492        &self.0
493    }
494    pub fn into_inner(self) -> String {
495        self.0
496    }
497}
498
499impl std::fmt::Display for DocumentUri {
500    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501        f.write_str(&self.0)
502    }
503}
504
505impl AsRef<str> for DocumentUri {
506    fn as_ref(&self) -> &str {
507        &self.0
508    }
509}
510
511impl std::ops::Deref for DocumentUri {
512    type Target = str;
513    fn deref(&self) -> &Self::Target {
514        &self.0
515    }
516}
517
518impl From<String> for DocumentUri {
519    fn from(s: String) -> Self {
520        Self(s)
521    }
522}
523
524impl From<&str> for DocumentUri {
525    fn from(s: &str) -> Self {
526        Self(s.to_owned())
527    }
528}
529
530impl From<&String> for DocumentUri {
531    fn from(s: &String) -> Self {
532        Self(s.clone())
533    }
534}
535
536impl std::borrow::Borrow<str> for DocumentUri {
537    fn borrow(&self) -> &str {
538        &self.0
539    }
540}
541
542impl std::borrow::Borrow<String> for DocumentUri {
543    fn borrow(&self) -> &String {
544        &self.0
545    }
546}
547
548// ── Selector types ─────────────────────────────────────────────────────────
549
550/// 4-byte function selector (`keccak256(signature)[0..4]`).
551///
552/// Used for external/public functions, public state variable getters,
553/// and custom errors. Stored as 8-char lowercase hex without `0x` prefix,
554/// matching the format solc uses in AST `functionSelector` / `errorSelector`
555/// fields and in `evm.methodIdentifiers` values.
556///
557/// # Examples
558/// ```ignore
559/// FuncSelector::new("f3cd914c")   // PoolManager.swap
560/// FuncSelector::new("8da5cb5b")   // Ownable.owner
561/// FuncSelector::new("0d89438e")   // DelegateCallNotAllowed error
562/// ```
563#[derive(Debug, Clone, PartialEq, Eq, Hash)]
564pub struct FuncSelector(String);
565
566impl FuncSelector {
567    /// Wrap a raw 8-char hex string (no `0x` prefix).
568    pub fn new(hex: impl Into<String>) -> Self {
569        Self(hex.into())
570    }
571
572    /// The raw hex string (no `0x` prefix), e.g. `"f3cd914c"`.
573    pub fn as_hex(&self) -> &str {
574        &self.0
575    }
576
577    /// Display with `0x` prefix, e.g. `"0xf3cd914c"`.
578    pub fn to_prefixed(&self) -> String {
579        format!("0x{}", self.0)
580    }
581}
582
583impl std::fmt::Display for FuncSelector {
584    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
585        write!(f, "{}", self.0)
586    }
587}
588
589/// 32-byte event topic (`keccak256(signature)`).
590///
591/// Used for events. Stored as 64-char lowercase hex without `0x` prefix,
592/// matching the format solc uses in the AST `eventSelector` field.
593///
594/// # Examples
595/// ```ignore
596/// EventSelector::new("8be0079c...") // OwnershipTransferred
597/// ```
598#[derive(Debug, Clone, PartialEq, Eq, Hash)]
599pub struct EventSelector(String);
600
601impl EventSelector {
602    /// Wrap a raw 64-char hex string (no `0x` prefix).
603    pub fn new(hex: impl Into<String>) -> Self {
604        Self(hex.into())
605    }
606
607    /// The raw hex string (no `0x` prefix).
608    pub fn as_hex(&self) -> &str {
609        &self.0
610    }
611
612    /// Display with `0x` prefix.
613    pub fn to_prefixed(&self) -> String {
614        format!("0x{}", self.0)
615    }
616}
617
618impl std::fmt::Display for EventSelector {
619    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
620        write!(f, "{}", self.0)
621    }
622}
623
624/// A selector extracted from an AST declaration node.
625///
626/// Unifies [`FuncSelector`] (functions, errors, public variables) and
627/// [`EventSelector`] (events) into a single enum so callers can handle
628/// both with one match.
629#[derive(Debug, Clone, PartialEq, Eq, Hash)]
630pub enum Selector {
631    /// 4-byte selector for functions, public variables, and errors.
632    Func(FuncSelector),
633    /// 32-byte topic hash for events.
634    Event(EventSelector),
635}
636
637impl Selector {
638    /// The raw hex string (no `0x` prefix).
639    pub fn as_hex(&self) -> &str {
640        match self {
641            Selector::Func(s) => s.as_hex(),
642            Selector::Event(s) => s.as_hex(),
643        }
644    }
645
646    /// Display with `0x` prefix.
647    pub fn to_prefixed(&self) -> String {
648        match self {
649            Selector::Func(s) => s.to_prefixed(),
650            Selector::Event(s) => s.to_prefixed(),
651        }
652    }
653}
654
655impl std::fmt::Display for Selector {
656    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657        match self {
658            Selector::Func(s) => write!(f, "{s}"),
659            Selector::Event(s) => write!(f, "{s}"),
660        }
661    }
662}
663
664/// Canonical ABI method signature from `evm.methodIdentifiers`.
665///
666/// This is the full ABI-encoded signature string like
667/// `"swap((address,address,uint24,int24,address),(bool,int256,uint160),bytes)"`.
668/// Unlike Solidity source signatures (which use struct names like `PoolKey`),
669/// these use fully-expanded tuple types. They are also the keys used in
670/// solc's `userdoc` and `devdoc` output.
671///
672/// Paired with a [`FuncSelector`] via `evm.methodIdentifiers`:
673/// `{ "swap(...)": "f3cd914c" }`.
674#[derive(Debug, Clone, PartialEq, Eq, Hash)]
675pub struct MethodId(String);
676
677impl MethodId {
678    /// Wrap a canonical ABI signature string.
679    pub fn new(sig: impl Into<String>) -> Self {
680        Self(sig.into())
681    }
682
683    /// The canonical signature, e.g. `"swap((address,...),bytes)"`.
684    pub fn as_str(&self) -> &str {
685        &self.0
686    }
687
688    /// The function/error name (text before the first `(`).
689    pub fn name(&self) -> &str {
690        self.0.split('(').next().unwrap_or(&self.0)
691    }
692}
693
694impl std::fmt::Display for MethodId {
695    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
696        write!(f, "{}", self.0)
697    }
698}
699
700// ── Path interner ──────────────────────────────────────────────────────────
701//
702// Project-wide, append-only table that assigns canonical [`FileId`] values
703// from file paths.  Every [`CachedBuild`] translates solc's arbitrary
704// per-compilation file IDs into canonical IDs at construction time, so all
705// builds share the same ID space and merges never produce file-ID conflicts.
706
707/// Project-wide path interner.
708///
709/// Assigns deterministic [`FileId`] values based on file paths.  Once a
710/// path is interned it keeps its ID for the lifetime of the session.
711///
712/// The interner is append-only: new paths get monotonically increasing IDs
713/// and existing paths keep theirs.  This means canonical IDs are stable
714/// across compilations — a property that solc's own file IDs lack.
715///
716/// Lives on `ForgeLsp` behind `Arc<RwLock<PathInterner>>` so every
717/// `CachedBuild` (per-file or project-wide) can share it.
718#[derive(Debug, Clone)]
719pub struct PathInterner {
720    /// Canonical ID → file path.
721    paths: Vec<String>,
722    /// File path → canonical ID (reverse lookup).
723    path_to_id: HashMap<String, u64>,
724}
725
726impl PathInterner {
727    /// Create an empty interner.
728    pub fn new() -> Self {
729        Self {
730            paths: Vec::new(),
731            path_to_id: HashMap::new(),
732        }
733    }
734
735    /// Get or assign a canonical [`FileId`] for `path`.
736    ///
737    /// If `path` was interned before, the same ID is returned.
738    /// Otherwise a new ID is allocated (one higher than the current max).
739    pub fn intern(&mut self, path: &str) -> FileId {
740        if let Some(&id) = self.path_to_id.get(path) {
741            return FileId(id);
742        }
743        let id = self.paths.len() as u64;
744        self.paths.push(path.to_owned());
745        self.path_to_id.insert(path.to_owned(), id);
746        FileId(id)
747    }
748
749    /// Look up the file path for a canonical ID.
750    pub fn resolve(&self, id: FileId) -> Option<&str> {
751        self.paths.get(id.0 as usize).map(|s| s.as_str())
752    }
753
754    /// Number of interned paths.
755    pub fn len(&self) -> usize {
756        self.paths.len()
757    }
758
759    /// Whether the interner is empty.
760    pub fn is_empty(&self) -> bool {
761        self.paths.is_empty()
762    }
763
764    /// Build a [`SolcFileId`]-keyed remap table that translates solc's
765    /// per-compilation file IDs into canonical IDs.
766    ///
767    /// `solc_id_to_path` is the `source_id_to_path` map from a single
768    /// solc invocation (e.g. `{ "0": "/abs/src/Foo.sol", "3": "/abs/lib/Bar.sol" }`).
769    ///
770    /// Returns a map from solc file ID → canonical [`FileId`] that can be
771    /// used to rewrite `src` strings during [`CachedBuild::new()`].
772    pub fn build_remap(
773        &mut self,
774        solc_id_to_path: &HashMap<SolcFileId, String>,
775    ) -> HashMap<u64, FileId> {
776        let mut remap = HashMap::with_capacity(solc_id_to_path.len());
777        for (solc_id, path) in solc_id_to_path {
778            let solc_num: u64 = solc_id.as_str().parse().unwrap_or(u64::MAX);
779            let canonical = self.intern(path);
780            remap.insert(solc_num, canonical);
781        }
782        remap
783    }
784
785    /// Build a canonical `id_to_path_map` from the interner's current state.
786    ///
787    /// Returns `HashMap<SolcFileId, String>` in the same shape as the
788    /// existing `CachedBuild.id_to_path_map`, but using canonical IDs.
789    pub fn to_id_to_path_map(&self) -> HashMap<SolcFileId, String> {
790        self.paths
791            .iter()
792            .enumerate()
793            .map(|(i, path)| (SolcFileId::new(i.to_string()), path.clone()))
794            .collect()
795    }
796}
797
798impl Default for PathInterner {
799    fn default() -> Self {
800        Self::new()
801    }
802}
803
804#[cfg(test)]
805mod tests {
806    use super::*;
807
808    #[test]
809    fn test_source_loc_parse_valid() {
810        let loc = SourceLoc::parse("100:50:3").unwrap();
811        assert_eq!(loc.offset, 100);
812        assert_eq!(loc.length, 50);
813        assert_eq!(loc.file_id, FileId(3));
814        assert_eq!(loc.end(), 150);
815        assert_eq!(loc.file_id_str(), SolcFileId::new("3"));
816    }
817
818    #[test]
819    fn test_source_loc_parse_zero() {
820        let loc = SourceLoc::parse("0:0:0").unwrap();
821        assert_eq!(loc.offset, 0);
822        assert_eq!(loc.length, 0);
823        assert_eq!(loc.file_id, FileId(0));
824    }
825
826    #[test]
827    fn test_source_loc_parse_invalid_format() {
828        assert!(SourceLoc::parse("").is_none());
829        assert!(SourceLoc::parse("100").is_none());
830        assert!(SourceLoc::parse("100:50").is_none());
831        assert!(SourceLoc::parse("abc:50:3").is_none());
832        assert!(SourceLoc::parse("100:abc:3").is_none());
833        assert!(SourceLoc::parse("100:50:abc").is_none());
834    }
835
836    #[test]
837    fn test_source_loc_parse_rejects_extra_parts() {
838        assert!(SourceLoc::parse("100:50:3:extra").is_none());
839    }
840
841    #[test]
842    fn test_node_id_equality() {
843        assert_eq!(NodeId(42), NodeId(42));
844        assert_ne!(NodeId(42), NodeId(43));
845    }
846
847    #[test]
848    fn test_file_id_equality() {
849        assert_eq!(FileId(1), FileId(1));
850        assert_ne!(FileId(1), FileId(2));
851    }
852
853    #[test]
854    fn test_node_id_file_id_are_different_types() {
855        // This test documents the compile-time guarantee.
856        // NodeId(1) and FileId(1) are different types — they cannot be
857        // compared or used interchangeably.
858        let _n: NodeId = NodeId(1);
859        let _f: FileId = FileId(1);
860        // If you uncomment the following line, it won't compile:
861        // assert_ne!(_n, _f);
862    }
863
864    // ── Selector type tests ────────────────────────────────────────────
865
866    #[test]
867    fn test_func_selector_display() {
868        let sel = FuncSelector::new("f3cd914c");
869        assert_eq!(sel.as_hex(), "f3cd914c");
870        assert_eq!(sel.to_prefixed(), "0xf3cd914c");
871        assert_eq!(format!("{sel}"), "f3cd914c");
872    }
873
874    #[test]
875    fn test_func_selector_equality() {
876        assert_eq!(FuncSelector::new("f3cd914c"), FuncSelector::new("f3cd914c"));
877        assert_ne!(FuncSelector::new("f3cd914c"), FuncSelector::new("8da5cb5b"));
878    }
879
880    #[test]
881    fn test_event_selector_display() {
882        let sel =
883            EventSelector::new("8be0079c5114abcdef1234567890abcdef1234567890abcdef1234567890abcd");
884        assert_eq!(sel.as_hex().len(), 64);
885        assert!(sel.to_prefixed().starts_with("0x"));
886    }
887
888    #[test]
889    fn test_selector_enum_variants() {
890        let func = Selector::Func(FuncSelector::new("f3cd914c"));
891        let event = Selector::Event(EventSelector::new("a".repeat(64)));
892
893        assert_eq!(func.as_hex(), "f3cd914c");
894        assert_eq!(func.to_prefixed(), "0xf3cd914c");
895        assert_eq!(event.as_hex().len(), 64);
896    }
897
898    #[test]
899    fn test_method_id() {
900        let mid = MethodId::new(
901            "swap((address,address,uint24,int24,address),(bool,int256,uint160),bytes)",
902        );
903        assert_eq!(mid.name(), "swap");
904        assert!(mid.as_str().starts_with("swap("));
905    }
906
907    #[test]
908    fn test_method_id_no_params() {
909        let mid = MethodId::new("settle()");
910        assert_eq!(mid.name(), "settle");
911    }
912
913    #[test]
914    fn test_func_selector_hashmap_key() {
915        use std::collections::HashMap;
916        let mut map = HashMap::new();
917        map.insert(FuncSelector::new("f3cd914c"), "swap");
918        map.insert(FuncSelector::new("8da5cb5b"), "owner");
919        assert_eq!(map.get(&FuncSelector::new("f3cd914c")), Some(&"swap"));
920        assert_eq!(map.get(&FuncSelector::new("8da5cb5b")), Some(&"owner"));
921    }
922
923    // ── PathInterner tests ─────────────────────────────────────────────
924
925    #[test]
926    fn test_path_interner_basic() {
927        let mut interner = PathInterner::new();
928        assert!(interner.is_empty());
929
930        let id_a = interner.intern("src/Foo.sol");
931        let id_b = interner.intern("src/Bar.sol");
932        assert_ne!(id_a, id_b);
933        assert_eq!(interner.len(), 2);
934
935        // Same path returns the same ID.
936        let id_a2 = interner.intern("src/Foo.sol");
937        assert_eq!(id_a, id_a2);
938        assert_eq!(interner.len(), 2);
939    }
940
941    #[test]
942    fn test_path_interner_resolve() {
943        let mut interner = PathInterner::new();
944        let id = interner.intern("/abs/src/Pool.sol");
945        assert_eq!(interner.resolve(id), Some("/abs/src/Pool.sol"));
946        assert_eq!(interner.resolve(FileId(999)), None);
947    }
948
949    #[test]
950    fn test_path_interner_monotonic_ids() {
951        let mut interner = PathInterner::new();
952        let a = interner.intern("a.sol");
953        let b = interner.intern("b.sol");
954        let c = interner.intern("c.sol");
955        assert_eq!(a, FileId(0));
956        assert_eq!(b, FileId(1));
957        assert_eq!(c, FileId(2));
958    }
959
960    #[test]
961    fn test_path_interner_build_remap() {
962        let mut interner = PathInterner::new();
963        // Pre-intern one path from a previous compilation.
964        interner.intern("/abs/src/Foo.sol");
965
966        // Simulate solc output where file IDs are different.
967        let mut solc_map = HashMap::new();
968        solc_map.insert(SolcFileId::new("0"), "/abs/src/Bar.sol".to_string());
969        solc_map.insert(SolcFileId::new("1"), "/abs/src/Foo.sol".to_string());
970
971        let remap = interner.build_remap(&solc_map);
972
973        // Foo.sol was already interned as canonical 0.
974        assert_eq!(remap[&1], FileId(0));
975        // Bar.sol is new, gets canonical 1.
976        assert_eq!(remap[&0], FileId(1));
977    }
978
979    #[test]
980    fn test_path_interner_to_id_to_path_map() {
981        let mut interner = PathInterner::new();
982        interner.intern("src/A.sol");
983        interner.intern("src/B.sol");
984
985        let map = interner.to_id_to_path_map();
986        assert_eq!(map.get("0").map(|s| s.as_str()), Some("src/A.sol"));
987        assert_eq!(map.get("1").map(|s| s.as_str()), Some("src/B.sol"));
988    }
989}