1use crate::locals::{skip_string, skip_trivia};
16use crate::{find_matching_delimiter, find_solidity_keyword, is_identifier, parse_identifier};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct StructMember {
21 pub name: String,
22 pub ty: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27struct StructDecl {
28 name: String,
29 scope: Option<String>,
31 members: Vec<StructMember>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35struct EnumDecl {
36 name: String,
37 scope: Option<String>,
38 variants: Vec<String>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42struct ValueTypeDecl {
43 name: String,
44 scope: Option<String>,
45 underlying: String,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50struct Scope {
51 name: String,
52 source_id: u64,
53 start: usize,
54 end: usize,
55 bases: Vec<String>,
57}
58
59#[derive(Debug, Clone, Default, PartialEq, Eq)]
61pub struct SourceTypes {
62 structs: Vec<StructDecl>,
63 enums: Vec<EnumDecl>,
64 value_types: Vec<ValueTypeDecl>,
65 scopes: Vec<Scope>,
66}
67
68impl SourceTypes {
69 #[must_use]
71 pub fn parse(source: &str) -> Self {
72 let mut types = Self::default();
73 types.add_source(0, source);
74 types
75 }
76
77 pub fn add_source(&mut self, source_id: u64, source: &str) {
79 let code = code_ranges(source);
80 let scopes = scan_scopes(source, source_id, &code);
81 let scope_of = |offset: usize| -> Option<String> {
82 scopes
83 .iter()
84 .filter(|scope| scope.start <= offset && offset < scope.end)
85 .min_by_key(|scope| scope.end - scope.start)
86 .map(|scope| scope.name.clone())
87 };
88 let mut cursor = 0;
89 while let Some(at) = find_keyword(source, &code, "struct", cursor) {
90 cursor = at + "struct".len();
91 let Some((name, open, close)) = braced_declaration(source, cursor) else {
92 continue;
93 };
94 self.structs.push(StructDecl {
95 name: name.to_owned(),
96 scope: scope_of(at),
97 members: parse_members(&source[open + 1..close]),
98 });
99 cursor = close + 1;
100 }
101 let mut cursor = 0;
102 while let Some(at) = find_keyword(source, &code, "enum", cursor) {
103 cursor = at + "enum".len();
104 let Some((name, open, close)) = braced_declaration(source, cursor) else {
105 continue;
106 };
107 let variants = source[open + 1..close]
108 .split(',')
109 .map(str::trim)
110 .filter(|variant| !variant.is_empty())
111 .map(str::to_owned)
112 .collect();
113 self.enums.push(EnumDecl {
114 name: name.to_owned(),
115 scope: scope_of(at),
116 variants,
117 });
118 cursor = close + 1;
119 }
120 let mut cursor = 0;
121 while let Some(at) = find_keyword(source, &code, "type", cursor) {
122 cursor = at + "type".len();
123 let Some((name, name_end)) = parse_identifier(source, skip_trivia(source, cursor))
124 else {
125 continue;
126 };
127 let after = skip_trivia(source, name_end);
128 let Some(("is", is_end)) = parse_identifier(source, after) else {
129 continue;
130 };
131 let underlying_start = skip_trivia(source, is_end);
132 let Some(end) = source[underlying_start..].find(';') else {
133 continue;
134 };
135 let underlying = source[underlying_start..underlying_start + end].trim();
136 if underlying.is_empty() {
137 continue;
138 }
139 self.value_types.push(ValueTypeDecl {
140 name: name.to_owned(),
141 scope: scope_of(at),
142 underlying: underlying.to_owned(),
143 });
144 cursor = underlying_start + end;
145 }
146 self.scopes.extend(scopes);
147 }
148
149 #[must_use]
151 pub fn is_empty(&self) -> bool {
152 self.structs.is_empty() && self.enums.is_empty() && self.value_types.is_empty()
153 }
154
155 #[must_use]
158 pub fn scope_at(&self, source_id: u64, offset: u64) -> Option<&str> {
159 let offset = usize::try_from(offset).ok()?;
160 self.scopes
161 .iter()
162 .filter(|scope| {
163 scope.source_id == source_id && scope.start <= offset && offset < scope.end
164 })
165 .min_by_key(|scope| scope.end - scope.start)
166 .map(|scope| scope.name.as_str())
167 }
168
169 #[must_use]
172 pub fn struct_members(&self, scope: Option<&str>, name: &str) -> Option<&[StructMember]> {
173 self.resolve(
174 self.structs.iter(),
175 |decl| (&decl.name, decl.scope.as_deref()),
176 scope,
177 name,
178 )
179 .map(|decl| decl.members.as_slice())
180 }
181
182 #[must_use]
184 pub fn enum_variants(&self, scope: Option<&str>, name: &str) -> Option<&[String]> {
185 self.resolve(
186 self.enums.iter(),
187 |decl| (&decl.name, decl.scope.as_deref()),
188 scope,
189 name,
190 )
191 .map(|decl| decl.variants.as_slice())
192 }
193
194 #[must_use]
196 pub fn underlying(&self, scope: Option<&str>, name: &str) -> Option<&str> {
197 self.resolve(
198 self.value_types.iter(),
199 |decl| (&decl.name, decl.scope.as_deref()),
200 scope,
201 name,
202 )
203 .map(|decl| decl.underlying.as_str())
204 }
205
206 #[must_use]
208 pub fn enum_literal(&self, scope: Option<&str>, path: &str) -> Option<u64> {
209 let (enum_name, variant) = path.rsplit_once('.')?;
210 let variants = self.enum_variants(scope, enum_name)?;
211 variants
212 .iter()
213 .position(|candidate| candidate == variant)
214 .map(|index| index as u64)
215 }
216
217 #[must_use]
219 pub fn is_struct(&self, scope: Option<&str>, name: &str) -> bool {
220 self.struct_members(scope, name).is_some()
221 }
222
223 #[must_use]
228 pub fn enum_variants_by_name(&self) -> std::collections::BTreeMap<String, Vec<String>> {
229 let mut names = std::collections::BTreeMap::new();
230 for decl in &self.enums {
231 if let Some(scope) = &decl.scope {
232 names
233 .entry(format!("{scope}.{}", decl.name))
234 .or_insert_with(|| decl.variants.clone());
235 }
236 names
237 .entry(decl.name.clone())
238 .or_insert_with(|| decl.variants.clone());
239 }
240 names
241 }
242
243 fn bases(&self, scope: &str) -> Vec<&str> {
245 let mut seen = Vec::<&str>::new();
246 let mut queue = self
247 .scopes
248 .iter()
249 .filter(|candidate| candidate.name == scope)
250 .flat_map(|candidate| candidate.bases.iter().map(String::as_str))
251 .collect::<std::collections::VecDeque<_>>();
252 while let Some(base) = queue.pop_front() {
253 if seen.contains(&base) {
254 continue;
255 }
256 seen.push(base);
257 queue.extend(
258 self.scopes
259 .iter()
260 .filter(|candidate| candidate.name == base)
261 .flat_map(|candidate| candidate.bases.iter().map(String::as_str)),
262 );
263 }
264 seen
265 }
266
267 fn resolve<'a, T>(
271 &'a self,
272 declarations: impl Iterator<Item = &'a T> + Clone,
273 key: impl Fn(&'a T) -> (&'a String, Option<&'a str>),
274 scope: Option<&str>,
275 name: &str,
276 ) -> Option<&'a T> {
277 let (qualifier, bare) = match name.rsplit_once('.') {
278 Some((qualifier, bare)) => (Some(qualifier), bare),
279 None => (None, name),
280 };
281 let candidates = declarations
282 .filter(|declaration| key(declaration).0 == bare)
283 .collect::<Vec<_>>();
284 if candidates.is_empty() {
285 return None;
286 }
287 let in_scope = |wanted: Option<&str>| {
288 candidates
289 .iter()
290 .copied()
291 .find(|declaration| key(declaration).1 == wanted)
292 };
293 if let Some(qualifier) = qualifier {
294 return in_scope(Some(qualifier)).or_else(|| candidates.first().copied());
297 }
298 if let Some(scope) = scope {
299 if let Some(found) = in_scope(Some(scope)) {
300 return Some(found);
301 }
302 for base in self.bases(scope) {
303 if let Some(found) = in_scope(Some(base)) {
304 return Some(found);
305 }
306 }
307 }
308 in_scope(None).or_else(|| candidates.first().copied())
309 }
310}
311
312fn code_ranges(source: &str) -> Vec<(usize, usize)> {
314 let bytes = source.as_bytes();
315 let mut ranges = Vec::new();
316 let mut index = 0;
317 let mut start = 0;
318 while index < bytes.len() {
319 let skipped = skip_trivia(source, index);
320 if skipped != index {
321 if start < index {
322 ranges.push((start, index));
323 }
324 index = skipped;
325 start = index;
326 continue;
327 }
328 match bytes[index] {
329 b'"' | b'\'' => {
330 if start < index {
331 ranges.push((start, index));
332 }
333 index = skip_string(source, index);
334 start = index;
335 }
336 _ => index += 1,
337 }
338 }
339 if start < bytes.len() {
340 ranges.push((start, bytes.len()));
341 }
342 ranges
343}
344
345fn find_keyword(
347 source: &str,
348 code: &[(usize, usize)],
349 keyword: &str,
350 start: usize,
351) -> Option<usize> {
352 let mut cursor = start;
353 while let Some(at) = find_solidity_keyword(source, keyword, cursor) {
354 if code.iter().any(|(from, to)| *from <= at && at < *to) {
355 return Some(at);
356 }
357 cursor = at + keyword.len();
358 }
359 None
360}
361
362fn scan_scopes(source: &str, source_id: u64, code: &[(usize, usize)]) -> Vec<Scope> {
364 let mut scopes = Vec::new();
365 for keyword in ["contract", "library", "interface"] {
366 let mut cursor = 0;
367 while let Some(at) = find_keyword(source, code, keyword, cursor) {
368 cursor = at + keyword.len();
369 let Some((name, name_end)) = parse_identifier(source, skip_trivia(source, cursor))
370 else {
371 continue;
372 };
373 let Some(open) = source[name_end..].find('{').map(|found| name_end + found) else {
376 continue;
377 };
378 let header = &source[name_end..open];
379 if header.contains(';') {
380 continue;
381 }
382 let Some(close) = find_matching_delimiter(source, open, b'{', b'}') else {
383 continue;
384 };
385 scopes.push(Scope {
386 name: name.to_owned(),
387 source_id,
388 start: open,
389 end: close,
390 bases: parse_bases(header),
391 });
392 cursor = open + 1;
393 }
394 }
395 scopes
396}
397
398fn parse_bases(header: &str) -> Vec<String> {
400 let trimmed = skip_trivia(header, 0);
401 let Some(rest) = header[trimmed..].strip_prefix("is") else {
402 return Vec::new();
403 };
404 if rest
405 .bytes()
406 .next()
407 .is_some_and(|byte| byte == b'_' || byte.is_ascii_alphanumeric())
408 {
409 return Vec::new();
410 }
411 let mut bases = Vec::new();
412 let mut depth = 0_i32;
413 let mut start = 0;
414 let bytes = rest.as_bytes();
415 for index in 0..=bytes.len() {
416 match bytes.get(index) {
417 Some(b'(') => {
418 if depth == 0 {
419 push_base(&mut bases, &rest[start..index]);
420 start = bytes.len();
421 }
422 depth += 1;
423 }
424 Some(b')') => depth -= 1,
425 Some(b',') | None if depth == 0 => {
426 if start < index {
427 push_base(&mut bases, &rest[start..index]);
428 }
429 start = index + 1;
430 }
431 _ => {}
432 }
433 }
434 bases
435}
436
437fn push_base(bases: &mut Vec<String>, text: &str) {
438 let name = text.trim();
439 if is_identifier(name) {
440 bases.push(name.to_owned());
441 }
442}
443
444fn braced_declaration(source: &str, after: usize) -> Option<(&str, usize, usize)> {
447 let (name, name_end) = parse_identifier(source, skip_trivia(source, after))?;
448 let open = skip_trivia(source, name_end);
449 if source.as_bytes().get(open) != Some(&b'{') {
450 return None;
451 }
452 let close = find_matching_delimiter(source, open, b'{', b'}')?;
453 Some((name, open, close))
454}
455
456fn parse_members(body: &str) -> Vec<StructMember> {
458 let mut members = Vec::new();
459 let mut index = 0;
460 while index < body.len() {
461 index = skip_trivia(body, index);
462 if index >= body.len() {
463 break;
464 }
465 let Some(end) = find_member_end(body, index) else {
466 break;
467 };
468 if let Some((ty, name)) = split_member(&body[index..end]) {
469 members.push(StructMember { name, ty });
470 }
471 index = end + 1;
472 }
473 members
474}
475
476fn split_member(text: &str) -> Option<(String, String)> {
478 let text = text.trim();
479 let name_start = text
480 .rfind(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
481 .map_or(0, |index| index + 1);
482 let name = &text[name_start..];
483 let ty = text[..name_start]
484 .split_whitespace()
485 .collect::<Vec<_>>()
486 .join(" ");
487 if !is_identifier(name) || ty.is_empty() {
488 return None;
489 }
490 Some((ty, name.to_owned()))
491}
492
493fn find_member_end(body: &str, index: usize) -> Option<usize> {
496 let bytes = body.as_bytes();
497 let mut depth = 0_i32;
498 for (offset, byte) in bytes.iter().enumerate().skip(index) {
499 match byte {
500 b'(' | b'[' => depth += 1,
501 b')' | b']' => depth -= 1,
502 b';' if depth <= 0 => return Some(offset),
503 _ => {}
504 }
505 }
506 None
507}
508
509#[cfg(test)]
510mod tests {
511 use super::{SourceTypes, StructMember};
512
513 const SOURCE: &str = "\
514// SPDX-License-Identifier: MIT
515pragma solidity ^0.8.20;
516
517type Price is uint128;
518
519struct Pair {
520 uint256 a;
521 uint256 b;
522}
523
524enum Level { Low, High }
525
526contract Shop {
527 enum Color { Red, Green, Blue }
528 struct Item {
529 uint256 id;
530 string name;
531 Color color;
532 uint256[] tags;
533 mapping(address => uint256) owners;
534 Pair[2] pairs;
535 }
536 type Money is uint256;
537 function f() public pure returns (uint256) { return 0; } // struct in a comment
538}
539
540library Math {
541 struct Item { uint8 x; }
542}
543
544abstract contract Base is Shop {
545 struct Item { uint16 y; }
546}
547
548contract Outlet is Base, Math(1) {
549 function g() public pure returns (uint256) { return 1; }
550}
551";
552
553 #[test]
554 fn finds_structs_enums_and_value_types_with_their_scopes() {
555 let types = SourceTypes::parse(SOURCE);
556 assert_eq!(
557 types.struct_members(None, "Pair"),
558 Some(
559 &[
560 StructMember {
561 name: "a".to_owned(),
562 ty: "uint256".to_owned()
563 },
564 StructMember {
565 name: "b".to_owned(),
566 ty: "uint256".to_owned()
567 },
568 ][..]
569 )
570 );
571 let item = types.struct_members(None, "Shop.Item").expect("Shop.Item");
572 assert_eq!(
573 item.iter()
574 .map(|member| (member.name.as_str(), member.ty.as_str()))
575 .collect::<Vec<_>>(),
576 [
577 ("id", "uint256"),
578 ("name", "string"),
579 ("color", "Color"),
580 ("tags", "uint256[]"),
581 ("owners", "mapping(address => uint256)"),
582 ("pairs", "Pair[2]"),
583 ]
584 );
585 assert_eq!(
586 types.struct_members(None, "Math.Item").map(<[_]>::len),
587 Some(1)
588 );
589 assert_eq!(
590 types
591 .enum_variants(None, "Color")
592 .map(|variants| variants.join(",")),
593 Some("Red,Green,Blue".to_owned())
594 );
595 assert_eq!(
596 types
597 .enum_variants(None, "Level")
598 .map(|variants| variants.join(",")),
599 Some("Low,High".to_owned())
600 );
601 assert_eq!(types.underlying(None, "Price"), Some("uint128"));
602 assert_eq!(types.underlying(None, "Shop.Money"), Some("uint256"));
603 assert_eq!(types.enum_literal(None, "Color.Green"), Some(1));
604 assert_eq!(types.enum_literal(None, "Shop.Color.Blue"), Some(2));
605 assert_eq!(types.enum_literal(None, "Color.Purple"), None);
606 assert_eq!(types.enum_literal(None, "Missing.Red"), None);
607 assert!(types.struct_members(None, "Nothing").is_none());
608 let names = types.enum_variants_by_name();
609 assert_eq!(names["Shop.Color"], ["Red", "Green", "Blue"]);
610 assert_eq!(names["Color"], ["Red", "Green", "Blue"]);
611 assert_eq!(names["Level"], ["Low", "High"]);
612 }
613
614 #[test]
615 fn a_bare_name_resolves_in_the_executing_contract_then_its_bases() {
616 let types = SourceTypes::parse(SOURCE);
617 let width = |scope: Option<&str>| {
619 types
620 .struct_members(scope, "Item")
621 .map(|members| members[0].ty.clone())
622 };
623 assert_eq!(width(Some("Shop")).as_deref(), Some("uint256"));
624 assert_eq!(width(Some("Math")).as_deref(), Some("uint8"));
625 assert_eq!(width(Some("Base")).as_deref(), Some("uint16"));
626 assert_eq!(width(Some("Outlet")).as_deref(), Some("uint16"));
628 assert_eq!(width(Some("Other")).as_deref(), Some("uint256"));
630 assert_eq!(width(None).as_deref(), Some("uint256"));
631 assert_eq!(
633 types.struct_members(Some("Math"), "Pair").map(<[_]>::len),
634 Some(2)
635 );
636 assert_eq!(types.enum_literal(Some("Outlet"), "Color.Blue"), Some(2));
638 let offset = SOURCE.find("function g()").expect("g") as u64;
640 assert_eq!(types.scope_at(0, offset), Some("Outlet"));
641 assert_eq!(types.scope_at(0, 0), None);
642 assert_eq!(types.scope_at(1, offset), None);
643 }
644
645 #[test]
646 fn ignores_keywords_inside_identifiers_and_comments() {
647 let types = SourceTypes::parse(
648 "contract C { uint256 mystruct; /* struct Hidden { uint256 x; } */ enum E { A } }",
649 );
650 assert!(types.struct_members(None, "Hidden").is_none());
651 assert_eq!(types.enum_variants(None, "E").map(<[_]>::len), Some(1));
652 }
653}