Skip to main content

lintspec_core/parser/
slang.rs

1//! A parser with [`slang_solidity`] as the backend
2//!
3//! This parser is now "legacy" as [`solar_parse`] is much faster.
4use std::{
5    collections::HashMap,
6    io,
7    path::{Path, PathBuf},
8    sync::{Arc, Mutex},
9};
10
11use slang_solidity::{
12    cst::{
13        Cursor, NonterminalKind, Query, QueryMatch, TerminalKind, TextIndex as SlangTextIndex,
14        TextRange as SlangTextRange,
15    },
16    parser::Parser,
17};
18
19use crate::{
20    definitions::{
21        Attributes, Definition, Identifier, Parent, Visibility, constructor::ConstructorDefinition,
22        contract::ContractDefinition, enumeration::EnumDefinition, error::ErrorDefinition,
23        event::EventDefinition, function::FunctionDefinition, interface::InterfaceDefinition,
24        library::LibraryDefinition, modifier::ModifierDefinition, structure::StructDefinition,
25        variable::VariableDeclaration,
26    },
27    error::{ErrorKind, Result},
28    interner::INTERNER,
29    natspec::{NatSpec, parse_comment},
30    parser::DocumentId,
31    prelude::OrPanic as _,
32    textindex::{TextIndex, TextRange},
33    utils::{detect_solidity_version, get_latest_supported_version},
34};
35
36use super::{Parse, ParsedDocument, complete_text_ranges};
37
38/// A parser that uses [`slang_solidity`] to identify source items
39#[derive(Debug, Clone, Default, bon::Builder)]
40#[non_exhaustive]
41pub struct SlangParser {
42    #[builder(default)]
43    pub skip_version_detection: bool,
44
45    #[builder(skip)]
46    documents: Arc<Mutex<HashMap<DocumentId, String>>>,
47}
48
49impl SlangParser {
50    /// The `slang` queries for all source items
51    #[must_use]
52    pub fn queries() -> Vec<Query> {
53        vec![
54            ConstructorDefinition::query(),
55            EnumDefinition::query(),
56            ErrorDefinition::query(),
57            EventDefinition::query(),
58            FunctionDefinition::query(),
59            ModifierDefinition::query(),
60            StructDefinition::query(),
61            VariableDeclaration::query(),
62            ContractDefinition::query(),
63            InterfaceDefinition::query(),
64            LibraryDefinition::query(),
65        ]
66    }
67
68    /// Find source item definitions from a root CST [`Cursor`]
69    #[must_use]
70    pub fn find_items(cursor: Cursor) -> Vec<Definition> {
71        let mut out = Vec::new();
72        for m in cursor.query(Self::queries()) {
73            let def = match m.query_index() {
74                0 => Some(
75                    ConstructorDefinition::extract(m)
76                        .unwrap_or_else(|e| Definition::NatspecParsingError(e.into_inner())),
77                ),
78                1 => Some(
79                    EnumDefinition::extract(m)
80                        .unwrap_or_else(|e| Definition::NatspecParsingError(e.into_inner())),
81                ),
82                2 => Some(
83                    ErrorDefinition::extract(m)
84                        .unwrap_or_else(|e| Definition::NatspecParsingError(e.into_inner())),
85                ),
86                3 => Some(
87                    EventDefinition::extract(m)
88                        .unwrap_or_else(|e| Definition::NatspecParsingError(e.into_inner())),
89                ),
90                4 => {
91                    let def = FunctionDefinition::extract(m)
92                        .unwrap_or_else(|e| Definition::NatspecParsingError(e.into_inner()));
93                    if out.contains(&def) { None } else { Some(def) }
94                }
95                5 => {
96                    let def = ModifierDefinition::extract(m)
97                        .unwrap_or_else(|e| Definition::NatspecParsingError(e.into_inner()));
98                    if out.contains(&def) { None } else { Some(def) }
99                }
100                6 => Some(
101                    StructDefinition::extract(m)
102                        .unwrap_or_else(|e| Definition::NatspecParsingError(e.into_inner())),
103                ),
104                7 => Some(
105                    VariableDeclaration::extract(m)
106                        .unwrap_or_else(|e| Definition::NatspecParsingError(e.into_inner())),
107                ),
108                8 => {
109                    let def = ContractDefinition::extract(m)
110                        .unwrap_or_else(|e| Definition::NatspecParsingError(e.into_inner()));
111                    if out.contains(&def) { None } else { Some(def) }
112                }
113                9 => {
114                    let def = InterfaceDefinition::extract(m)
115                        .unwrap_or_else(|e| Definition::NatspecParsingError(e.into_inner()));
116                    if out.contains(&def) { None } else { Some(def) }
117                }
118                10 => Some(
119                    LibraryDefinition::extract(m)
120                        .unwrap_or_else(|e| Definition::NatspecParsingError(e.into_inner())),
121                ),
122                _ => unreachable!(),
123            };
124            if let Some(def) = def {
125                out.push(def);
126            }
127        }
128        out
129    }
130}
131
132impl Parse for SlangParser {
133    fn parse_document(
134        &mut self,
135        input: impl io::Read,
136        path: Option<impl AsRef<Path>>,
137        keep_contents: bool,
138    ) -> Result<ParsedDocument> {
139        fn inner(
140            this: &mut SlangParser,
141            mut input: impl io::Read,
142            path: Option<PathBuf>,
143            keep_contents: bool,
144        ) -> Result<ParsedDocument> {
145            let path = path.unwrap_or(PathBuf::from("<stdin>"));
146            let mut source = String::new();
147            input
148                .read_to_string(&mut source)
149                .map_err(|err| ErrorKind::IOError {
150                    path: PathBuf::new(),
151                    err,
152                })?;
153            let solidity_version = if this.skip_version_detection {
154                get_latest_supported_version()
155            } else {
156                detect_solidity_version(&source, &path)?
157            };
158            let parser = Parser::create(solidity_version).or_panic("parser should initialize");
159            let output = parser.parse_file_contents(&source);
160            if !output.is_valid() {
161                let Some(error) = output.errors().first() else {
162                    return Err(ErrorKind::UnknownError.into());
163                };
164                return Err(ErrorKind::ParsingError {
165                    path,
166                    loc: error.text_range().start.into(),
167                    message: error.message(),
168                }
169                .into());
170            }
171            let document_id = DocumentId::new();
172            let cursor = output.create_tree_cursor();
173            let mut definitions = SlangParser::find_items(cursor);
174            complete_text_ranges(&source, &mut definitions);
175            if keep_contents {
176                let mut documents = this
177                    .documents
178                    .lock()
179                    .or_panic("mutex should not be poisoned");
180                documents.insert(document_id, source);
181            }
182            Ok(ParsedDocument {
183                definitions,
184                id: document_id,
185            })
186        }
187        inner(
188            self,
189            input,
190            path.map(|p| p.as_ref().to_path_buf()),
191            keep_contents,
192        )
193    }
194
195    fn get_sources(self) -> Result<HashMap<DocumentId, String>> {
196        Ok(Arc::try_unwrap(self.documents)
197            .map_err(|_| ErrorKind::DanglingParserReferences)?
198            .into_inner()
199            .or_panic("mutex should not be poisoned"))
200    }
201}
202
203/// A trait to extract definitions from a [`slang_solidity`] CST
204pub trait Extract {
205    /// Return a [`slang_solidity`] [`Query`] used to extract information about the source item
206    fn query() -> Query;
207
208    /// Extract information from the query matches
209    fn extract(m: QueryMatch) -> Result<Definition>;
210}
211
212impl Extract for ConstructorDefinition {
213    fn query() -> Query {
214        Query::create(
215            "@constructor [ConstructorDefinition
216            parameters:[ParametersDeclaration
217                @constructor_params parameters:[Parameters]
218            ]
219            @constructor_attr attributes:[ConstructorAttributes]
220        ]",
221        )
222        .or_panic("query should compile")
223    }
224
225    fn extract(m: QueryMatch) -> Result<Definition> {
226        let constructor = capture(&m, "constructor")?;
227        let params = capture(&m, "constructor_params")?;
228        let attr = capture(&m, "constructor_attr")?;
229
230        let span_start = find_definition_start(&constructor);
231        let span_end = attr.text_range().end.into();
232        let span = span_start..span_end;
233
234        let params = extract_params(&params, NonterminalKind::Parameter);
235        let natspec = extract_comment(&constructor.clone(), &[])?;
236        let parent = extract_parent_name(constructor);
237
238        Ok(ConstructorDefinition {
239            parent,
240            span,
241            params,
242            natspec,
243        }
244        .into())
245    }
246}
247
248impl Extract for EnumDefinition {
249    fn query() -> Query {
250        Query::create(
251            "@enum [EnumDefinition
252            @enum_name name:[Identifier]
253            @enum_members members:[EnumMembers]
254        ]",
255        )
256        .or_panic("query should compile")
257    }
258
259    fn extract(m: QueryMatch) -> Result<Definition> {
260        let enumeration = capture(&m, "enum")?;
261        let name = capture(&m, "enum_name")?;
262        let members = capture(&m, "enum_members")?;
263
264        let span = find_definition_start(&enumeration)..find_definition_end(&enumeration);
265        let name = INTERNER.get_or_intern(name.node().unparse().trim());
266        let members = extract_enum_members(&members);
267        let natspec = extract_comment(&enumeration.clone(), &[])?;
268        let parent = extract_parent_name(enumeration);
269
270        Ok(EnumDefinition {
271            parent,
272            name,
273            span,
274            members,
275            natspec,
276        }
277        .into())
278    }
279}
280
281impl Extract for ErrorDefinition {
282    fn query() -> Query {
283        Query::create(
284            "@err [ErrorDefinition
285            @err_name name:[Identifier]
286            @err_params members:[ErrorParametersDeclaration]
287        ]",
288        )
289        .or_panic("query should compile")
290    }
291
292    fn extract(m: QueryMatch) -> Result<Definition> {
293        let err = capture(&m, "err")?;
294        let name = capture(&m, "err_name")?;
295        let params = capture(&m, "err_params")?;
296
297        let span = find_definition_start(&err)..find_definition_end(&err);
298        let name = INTERNER.get_or_intern(name.node().unparse().trim());
299        let params = extract_identifiers(&params);
300        let natspec = extract_comment(&err.clone(), &[])?;
301        let parent = extract_parent_name(err);
302
303        Ok(ErrorDefinition {
304            parent,
305            name,
306            span,
307            params,
308            natspec,
309        }
310        .into())
311    }
312}
313
314impl Extract for EventDefinition {
315    fn query() -> Query {
316        Query::create(
317            "@event [EventDefinition
318            @event_name name:[Identifier]
319            @event_params parameters:[EventParametersDeclaration]
320        ]",
321        )
322        .or_panic("query should compile")
323    }
324
325    fn extract(m: QueryMatch) -> Result<Definition> {
326        let event = capture(&m, "event")?;
327        let name = capture(&m, "event_name")?;
328        let params = capture(&m, "event_params")?;
329
330        let span = find_definition_start(&event)..find_definition_end(&event);
331        let name = INTERNER.get_or_intern(name.node().unparse().trim());
332        let params = extract_params(&params, NonterminalKind::EventParameter);
333        let natspec = extract_comment(&event.clone(), &[])?;
334        let parent = extract_parent_name(event);
335
336        Ok(EventDefinition {
337            parent,
338            name,
339            span,
340            params,
341            natspec,
342        }
343        .into())
344    }
345}
346
347impl Extract for FunctionDefinition {
348    fn query() -> Query {
349        Query::create(
350            "@function [FunctionDefinition
351            @keyword function_keyword:[FunctionKeyword]
352            @function_name name:[FunctionName]
353            parameters:[ParametersDeclaration
354                @function_params parameters:[Parameters]
355            ]
356            @function_attr attributes:[FunctionAttributes]
357            returns:[ReturnsDeclaration
358                @function_returns_declaration variables:[ParametersDeclaration
359                    @function_returns parameters:[Parameters]
360                ]
361            ]?
362        ]",
363        )
364        .or_panic("query should compile")
365    }
366
367    fn extract(m: QueryMatch) -> Result<Definition> {
368        let func = capture(&m, "function")?;
369        let name = capture(&m, "function_name")?;
370        let params = capture(&m, "function_params")?;
371        let attributes = capture(&m, "function_attr")?;
372        let returns_declaration = capture_opt(&m, "function_returns_declaration")?;
373        let returns = capture_opt(&m, "function_returns")?;
374
375        let span_start = find_definition_start(&func);
376        let span_end = returns_declaration
377            .as_ref()
378            .map_or_else(|| attributes.text_range().end.into(), find_definition_end);
379        let span = span_start..span_end;
380        let name = INTERNER.get_or_intern(name.node().unparse().trim());
381        let params = extract_params(&params, NonterminalKind::Parameter);
382        let returns = returns
383            .map(|r| extract_params(&r, NonterminalKind::Parameter))
384            .unwrap_or_default();
385        let natspec = extract_comment(&func.clone(), &returns)?;
386        let parent = extract_parent_name(func);
387
388        Ok(FunctionDefinition {
389            parent,
390            name,
391            span,
392            params,
393            returns,
394            natspec,
395            attributes: extract_attributes(&attributes),
396        }
397        .into())
398    }
399}
400
401impl Extract for ModifierDefinition {
402    fn query() -> Query {
403        Query::create(
404            "@modifier [ModifierDefinition
405            @modifier_name name:[Identifier]
406            parameters:[ParametersDeclaration
407                @modifier_params parameters:[Parameters]
408            ]?
409            @modifier_attr attributes:[ModifierAttributes]
410        ]",
411        )
412        .or_panic("query should compile")
413    }
414
415    fn extract(m: QueryMatch) -> Result<Definition> {
416        let modifier = capture(&m, "modifier")?;
417        let name = capture(&m, "modifier_name")?;
418        let params = capture_opt(&m, "modifier_params")?;
419        let attr = capture(&m, "modifier_attr")?;
420
421        let span_start = find_definition_start(&modifier);
422        let span_end = attr.text_range().end.into();
423        let span = span_start..span_end;
424        let name = INTERNER.get_or_intern(name.node().unparse().trim());
425        let params = params
426            .map(|p| extract_params(&p, NonterminalKind::Parameter))
427            .unwrap_or_default();
428
429        let natspec = extract_comment(&modifier.clone(), &[])?;
430        let parent = extract_parent_name(modifier);
431
432        Ok(ModifierDefinition {
433            parent,
434            name,
435            span,
436            params,
437            natspec,
438            attributes: extract_attributes(&attr),
439        }
440        .into())
441    }
442}
443
444impl Extract for StructDefinition {
445    fn query() -> Query {
446        Query::create(
447            "@struct [StructDefinition
448            @struct_name name:[Identifier]
449            @struct_members members:[StructMembers]
450        ]",
451        )
452        .or_panic("query should compile")
453    }
454
455    fn extract(m: QueryMatch) -> Result<Definition> {
456        let structure = capture(&m, "struct")?;
457        let name = capture(&m, "struct_name")?;
458        let members = capture(&m, "struct_members")?;
459
460        let span = find_definition_start(&structure)..find_definition_end(&structure);
461        let name = INTERNER.get_or_intern(name.node().unparse().trim());
462        let members = extract_struct_members(&members)?;
463        let natspec = extract_comment(&structure.clone(), &[])?;
464        let parent = extract_parent_name(structure);
465
466        Ok(StructDefinition {
467            parent,
468            name,
469            span,
470            members,
471            natspec,
472        }
473        .into())
474    }
475}
476
477impl Extract for VariableDeclaration {
478    fn query() -> Query {
479        Query::create(
480            "@variable [StateVariableDefinition
481            @variable_attr attributes:[StateVariableAttributes]
482            @variable_name name:[Identifier]
483        ]",
484        )
485        .or_panic("query should compile")
486    }
487
488    fn extract(m: QueryMatch) -> Result<Definition> {
489        let variable = capture(&m, "variable")?;
490        let attributes = capture(&m, "variable_attr")?;
491        let name = capture(&m, "variable_name")?;
492
493        let span = find_definition_start(&variable)..find_definition_end(&variable);
494        let name = INTERNER.get_or_intern(name.node().unparse().trim());
495        let natspec = extract_comment(&variable.clone(), &[])?;
496        let parent = extract_parent_name(variable);
497
498        Ok(VariableDeclaration {
499            parent,
500            name,
501            span,
502            natspec,
503            attributes: extract_attributes(&attributes),
504        }
505        .into())
506    }
507}
508
509impl Extract for ContractDefinition {
510    fn query() -> Query {
511        Query::create(
512            "@contract [ContractDefinition
513            @contract_name name:[Identifier]
514            @contract_spec inheritance:[InheritanceSpecifier]?
515        ]",
516        )
517        .or_panic("query should compile")
518    }
519
520    fn extract(m: QueryMatch) -> Result<Definition> {
521        let contract = capture(&m, "contract")?;
522        let name = capture(&m, "contract_name")?;
523        let spec = capture_opt(&m, "contract_spec")?;
524
525        let span_start = find_definition_start(&contract);
526        let span_end = spec
527            .as_ref()
528            .map_or_else(|| name.text_range().end.into(), find_definition_end);
529        let span = span_start..span_end;
530        let name = INTERNER.get_or_intern(name.node().unparse().trim());
531        let natspec = extract_comment(&contract.clone(), &[])?;
532
533        Ok(ContractDefinition {
534            name,
535            span,
536            natspec,
537        }
538        .into())
539    }
540}
541
542impl Extract for InterfaceDefinition {
543    fn query() -> Query {
544        Query::create(
545            "@iface [InterfaceDefinition
546            @iface_name name:[Identifier]
547            @iface_spec inheritance:[InheritanceSpecifier]?
548        ]",
549        )
550        .or_panic("query should compile")
551    }
552
553    fn extract(m: QueryMatch) -> Result<Definition> {
554        let iface = capture(&m, "iface")?;
555        let name = capture(&m, "iface_name")?;
556        let spec = capture_opt(&m, "iface_spec")?;
557
558        let span_start = find_definition_start(&iface);
559        let span_end = spec
560            .as_ref()
561            .map_or_else(|| name.text_range().end.into(), find_definition_end);
562        let span = span_start..span_end;
563        let name = INTERNER.get_or_intern(name.node().unparse().trim());
564        let natspec = extract_comment(&iface.clone(), &[])?;
565
566        Ok(InterfaceDefinition {
567            name,
568            span,
569            natspec,
570        }
571        .into())
572    }
573}
574
575impl Extract for LibraryDefinition {
576    fn query() -> Query {
577        Query::create(
578            "@library [LibraryDefinition
579            @library_name name:[Identifier]
580        ]",
581        )
582        .or_panic("query should compile")
583    }
584
585    fn extract(m: QueryMatch) -> Result<Definition> {
586        let library = capture(&m, "library")?;
587        let name = capture(&m, "library_name")?;
588
589        let span = find_definition_start(&library)..name.text_range().end.into();
590        let name = INTERNER.get_or_intern(name.node().unparse().trim());
591        let natspec = extract_comment(&library.clone(), &[])?;
592
593        Ok(LibraryDefinition {
594            name,
595            span,
596            natspec,
597        }
598        .into())
599    }
600}
601
602/// Retrieve and unwrap the first capture of a parser match, or return with an [`Error`](crate::error::Error)
603pub fn capture(m: &QueryMatch, name: &str) -> Result<Cursor> {
604    match m
605        .capture(name)
606        .map(|capture| capture.cursors().first().cloned())
607    {
608        Some(Some(res)) => Ok(res),
609        _ => Err(ErrorKind::UnknownError.into()),
610    }
611}
612
613/// Retrieve and unwrap the first capture of a parser match if one exists.
614pub fn capture_opt(m: &QueryMatch, name: &str) -> Result<Option<Cursor>> {
615    match m
616        .capture(name)
617        .map(|capture| capture.cursors().first().cloned())
618    {
619        Some(Some(res)) => Ok(Some(res)),
620        Some(None) => Ok(None),
621        _ => Err(ErrorKind::UnknownError.into()),
622    }
623}
624
625/// Extract parameters from a function-like source item.
626///
627/// The node kind that holds the `Identifier` (`Parameter`, `EventParameter`) must be provided with `kind`.
628#[must_use]
629pub fn extract_params(cursor: &Cursor, kind: NonterminalKind) -> Vec<Identifier> {
630    let mut cursor = cursor.spawn();
631    let mut out = Vec::new();
632    while cursor.go_to_next_nonterminal_with_kind(kind) {
633        let mut sub_cursor = cursor.spawn();
634        let mut found = false;
635        while sub_cursor.go_to_next_terminal_with_kind(TerminalKind::Identifier) {
636            if sub_cursor.label().to_string() != "name" {
637                continue;
638            }
639            found = true;
640            out.push(Identifier {
641                name: Some(INTERNER.get_or_intern(sub_cursor.node().unparse().trim())),
642                span: textrange(sub_cursor.text_range()),
643            });
644        }
645        if !found {
646            out.push(Identifier {
647                name: None,
648                span: find_definition_start(&cursor)..find_definition_end(&cursor),
649            });
650        }
651    }
652    out
653}
654
655/// Extract and parse the [`NatSpec`] comment information, if any
656pub fn extract_comment(cursor: &Cursor, returns: &[Identifier]) -> Result<Option<NatSpec>> {
657    let mut cursor = cursor.spawn();
658    let mut items = Vec::new();
659    while cursor.go_to_next() {
660        if cursor.node().is_terminal_with_kinds(&[
661            TerminalKind::MultiLineNatSpecComment,
662            TerminalKind::SingleLineNatSpecComment,
663        ]) {
664            let comment = &cursor.node().unparse();
665            let mut trimmed = comment.trim_start();
666            if trimmed.starts_with("////") || trimmed.starts_with("/***") {
667                // avoid a parsing error in those cases, we simply ignore those as if they were non-NatSpec comments
668                continue;
669            }
670            items.push((
671                cursor.node().kind().to_string(), // the node type to differentiate multiline from single line
672                cursor.text_range().start.line, // the line number to remove unwanted single-line comments
673                parse_comment(&mut trimmed)
674                    .map_err(|e| ErrorKind::NatspecParsingError {
675                        parent: extract_parent_name(cursor.clone()),
676                        span: textrange(cursor.text_range()),
677                        message: e.to_string(),
678                    })?
679                    .populate_returns(returns),
680            ));
681        } else if cursor.node().is_terminal_with_kinds(&[
682            TerminalKind::ContractKeyword,
683            TerminalKind::InterfaceKeyword,
684            TerminalKind::LibraryKeyword,
685            TerminalKind::ConstructorKeyword,
686            TerminalKind::EnumKeyword,
687            TerminalKind::ErrorKeyword,
688            TerminalKind::EventKeyword,
689            TerminalKind::FunctionKeyword,
690            TerminalKind::ModifierKeyword,
691            TerminalKind::StructKeyword,
692        ]) | cursor
693            .node()
694            .is_nonterminal_with_kind(NonterminalKind::StateVariableAttributes)
695        {
696            // anything after this node should be ignored, because we enter the item's body
697            break;
698        }
699    }
700    if let Some("MultiLineNatSpecComment") = items.last().map(|(kind, _, _)| kind.as_str())
701        && let Some((_, _, natspec)) = items.pop()
702    {
703        // if the last comment is multiline, we ignore all previous comments
704        return Ok(Some(natspec));
705    }
706    // the last comment is single-line
707    // we need to take the comments (in reverse) up to an empty line or a multiline comment (exclusive)
708    let mut res = Vec::new();
709    let mut iter = items.into_iter().rev().peekable();
710    while let Some((_, item_line, item)) = iter.next() {
711        res.push(item);
712        if let Some((next_kind, next_line, _)) = iter.peek()
713            && (next_kind == "MultiLineNatSpecComment" || *next_line < item_line - 1)
714        {
715            // the next comments up should be ignored
716            break;
717        }
718    }
719    if res.is_empty() {
720        return Ok(None);
721    }
722    Ok(Some(res.into_iter().rev().fold(
723        NatSpec::default(),
724        |mut acc, mut i| {
725            acc.append(&mut i);
726            acc
727        },
728    )))
729}
730
731/// Extract identifiers from a CST node, filtered by label equal to `name`
732#[must_use]
733pub fn extract_identifiers(cursor: &Cursor) -> Vec<Identifier> {
734    let mut cursor = cursor.spawn();
735    let mut out = Vec::new();
736    while cursor.go_to_next_terminal_with_kind(TerminalKind::Identifier) {
737        if cursor.label().to_string() != "name" {
738            continue;
739        }
740        out.push(Identifier {
741            name: Some(INTERNER.get_or_intern(cursor.node().unparse().trim())),
742            span: textrange(cursor.text_range()),
743        });
744    }
745    out
746}
747
748/// Extract the attributes (visibility and override) from a function-like item or state variable
749#[must_use]
750pub fn extract_attributes(cursor: &Cursor) -> Attributes {
751    let mut cursor = cursor.spawn();
752    let mut out = Attributes::default();
753    while cursor.go_to_next_terminal_with_kinds(&[
754        TerminalKind::ExternalKeyword,
755        TerminalKind::InternalKeyword,
756        TerminalKind::PrivateKeyword,
757        TerminalKind::PublicKeyword,
758        TerminalKind::OverrideKeyword,
759    ]) {
760        match cursor
761            .node()
762            .as_terminal()
763            .or_panic("should be terminal kind")
764            .kind
765        {
766            TerminalKind::ExternalKeyword => out.visibility = Visibility::External,
767            TerminalKind::InternalKeyword => out.visibility = Visibility::Internal,
768            TerminalKind::PrivateKeyword => out.visibility = Visibility::Private,
769            TerminalKind::PublicKeyword => out.visibility = Visibility::Public,
770            TerminalKind::OverrideKeyword => out.r#override = true,
771            _ => unreachable!(),
772        }
773    }
774    out
775}
776
777/// Find the parent's name (contract, interface, library), if any
778#[must_use]
779pub fn extract_parent_name(mut cursor: Cursor) -> Option<Parent> {
780    while cursor.go_to_parent() {
781        if let Some(parent) = cursor.node().as_nonterminal_with_kinds(&[
782            NonterminalKind::ContractDefinition,
783            NonterminalKind::InterfaceDefinition,
784            NonterminalKind::LibraryDefinition,
785        ]) {
786            for child in &parent.children {
787                if child.is_terminal_with_kind(TerminalKind::Identifier) {
788                    let name = INTERNER
789                        .get_or_intern(child.node.unparse().trim())
790                        .resolve_with(&INTERNER);
791                    return Some(match parent.kind {
792                        NonterminalKind::ContractDefinition => Parent::Contract(name),
793                        NonterminalKind::InterfaceDefinition => Parent::Interface(name),
794                        NonterminalKind::LibraryDefinition => Parent::Library(name),
795                        _ => unreachable!(),
796                    });
797                }
798            }
799        }
800    }
801    None
802}
803
804/// Extract the identifiers of each of an enum's variants
805#[must_use]
806pub fn extract_enum_members(cursor: &Cursor) -> Vec<Identifier> {
807    let mut cursor = cursor.spawn();
808    let mut out = Vec::new();
809    while cursor.go_to_next_terminal_with_kind(TerminalKind::Identifier) {
810        out.push(Identifier {
811            name: Some(INTERNER.get_or_intern(cursor.node().unparse().trim())),
812            span: textrange(cursor.text_range()),
813        });
814    }
815    out
816}
817
818/// Extract the identifiers for each of a struct's members
819pub fn extract_struct_members(cursor: &Cursor) -> Result<Vec<Identifier>> {
820    let cursor = cursor.spawn();
821    let mut out = Vec::new();
822    let query = Query::create(
823        "[StructMember
824        @member_name name:[Identifier]
825    ]",
826    )
827    .or_panic("query should compile");
828    for m in cursor.query(vec![query]) {
829        let member_name = capture(&m, "member_name")?;
830        out.push(Identifier {
831            name: Some(INTERNER.get_or_intern(member_name.node().unparse().trim())),
832            span: textrange(member_name.text_range()),
833        });
834    }
835    Ok(out)
836}
837
838/// Find the end index of a definition by ignoring the trailing trivia
839#[must_use]
840pub fn find_definition_end(cursor: &Cursor) -> TextIndex {
841    let default = cursor.text_range().end.into();
842    let mut cursor = cursor.spawn();
843    if !cursor.go_to_last_child() {
844        return default;
845    }
846    if !cursor.node().is_trivia() {
847        return cursor.text_range().end.into();
848    }
849    while cursor.go_to_previous() {
850        if cursor.node().is_trivia() {
851            continue;
852        }
853        return cursor.text_range().end.into();
854    }
855    default
856}
857
858/// Find the start index of a definition by ignoring any leading whitespace trivia
859#[must_use]
860pub fn find_definition_start(cursor: &Cursor) -> TextIndex {
861    let default = cursor.text_range().start.into();
862    let mut cursor = cursor.spawn();
863    while cursor.go_to_next() {
864        if cursor.node().is_terminal_with_kinds(&[
865            TerminalKind::Whitespace,
866            TerminalKind::EndOfLine,
867            TerminalKind::SingleLineComment,
868            TerminalKind::MultiLineComment,
869        ]) {
870            continue;
871        }
872        // special case for state variables, since the doc-comment is inside of the type node for some reason
873        if cursor.node().is_nonterminal_with_kinds(&[
874            NonterminalKind::TypeName,
875            NonterminalKind::ElementaryType,
876            NonterminalKind::IdentifierPath, // Some state variables have an EndOfLine inside the type identifier
877        ]) {
878            continue;
879        }
880        if cursor.node().is_terminal_with_kinds(&[
881            TerminalKind::SingleLineNatSpecComment,
882            TerminalKind::MultiLineNatSpecComment,
883        ]) {
884            let comment = cursor.node().unparse();
885            let comment = comment.trim_start();
886            if comment.starts_with("////") || comment.starts_with("/***") {
887                // those should not be considered valid doc-comments, they are normal comments
888                continue;
889            }
890        }
891        return cursor.text_range().start.into();
892    }
893    default
894}
895
896/// Convert from a slang `TextRange` to this crate's equivalent
897#[must_use]
898pub fn textrange(value: SlangTextRange) -> TextRange {
899    value.start.into()..value.end.into()
900}
901
902impl From<SlangTextIndex> for TextIndex {
903    fn from(value: SlangTextIndex) -> Self {
904        Self {
905            utf8: value.utf8,
906            line: u32::try_from(value.line).or_panic("slang line number is too large for u32"),
907            col_utf32: u32::try_from(value.column)
908                .or_panic("slang column number is too large for u32"),
909            // col_utf8 and col_utf16 are filled by the completion step
910            col_utf8: 0,
911            col_utf16: 0,
912        }
913    }
914}
915
916#[cfg(test)]
917mod tests {
918    use std::{fs::File, ops::Range};
919
920    use similar_asserts::assert_eq;
921    use slang_solidity::{cst::Cursor, parser::Parser};
922
923    use crate::{
924        interner::INTERNER,
925        natspec::{NatSpecItem, NatSpecKind},
926        utils::detect_solidity_version,
927    };
928
929    use super::*;
930
931    fn parse_file(contents: &str) -> Cursor {
932        let solidity_version = detect_solidity_version(contents, PathBuf::new()).unwrap();
933        let parser = Parser::create(solidity_version).unwrap();
934        let output = parser.parse_file_contents(contents);
935        assert!(output.is_valid(), "{:?}", output.errors());
936        output.create_tree_cursor()
937    }
938
939    macro_rules! impl_find_contract_item {
940        ($fn_name:ident, $item_variant:path, $item_type:ty) => {
941            fn $fn_name<'a>(name: &str, items: &'a [Definition]) -> &'a $item_type {
942                items
943                    .iter()
944                    .find_map(|d| match d {
945                        $item_variant(def) if def.name == INTERNER.get_or_intern(name) => Some(def),
946                        _ => None,
947                    })
948                    .unwrap()
949            }
950        };
951    }
952    macro_rules! impl_find_item {
953        ($fn_name:ident, $item_variant:path, $item_type:ty) => {
954            fn $fn_name<'a>(
955                name: &str,
956                parent: Option<Parent>,
957                items: &'a [Definition],
958            ) -> &'a $item_type {
959                items
960                    .iter()
961                    .find_map(|d| match d {
962                        $item_variant(def)
963                            if def.name == INTERNER.get_or_intern(name) && def.parent == parent =>
964                        {
965                            Some(def)
966                        }
967                        _ => None,
968                    })
969                    .unwrap()
970            }
971        };
972    }
973
974    impl_find_contract_item!(find_contract, Definition::Contract, ContractDefinition);
975    impl_find_contract_item!(find_interface, Definition::Interface, InterfaceDefinition);
976    impl_find_contract_item!(find_library, Definition::Library, LibraryDefinition);
977    impl_find_item!(find_function, Definition::Function, FunctionDefinition);
978    impl_find_item!(find_variable, Definition::Variable, VariableDeclaration);
979    impl_find_item!(find_modifier, Definition::Modifier, ModifierDefinition);
980    impl_find_item!(find_error, Definition::Error, ErrorDefinition);
981    impl_find_item!(find_event, Definition::Event, EventDefinition);
982    impl_find_item!(find_struct, Definition::Struct, StructDefinition);
983    impl_find_item!(find_enum, Definition::Enumeration, EnumDefinition);
984
985    macro_rules! adjust_offset_windows {
986        ($byte_index:literal, $lines:literal) => {
987            if cfg!(windows) {
988                $byte_index + $lines
989            } else {
990                $byte_index
991            }
992        };
993    }
994
995    #[expect(clippy::cast_possible_truncation)]
996    fn single_line_textrange(range: Range<usize>) -> TextRange {
997        TextIndex {
998            utf8: range.start,
999            line: 0,
1000            col_utf8: range.start as u32,
1001            col_utf16: range.start as u32,
1002            col_utf32: range.start as u32,
1003        }..TextIndex {
1004            utf8: range.end,
1005            line: 0,
1006            col_utf8: range.end as u32,
1007            col_utf16: range.end as u32,
1008            col_utf32: range.end as u32,
1009        }
1010    }
1011
1012    #[test]
1013    fn test_parse_contract() {
1014        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1015        let items = SlangParser::find_items(cursor);
1016        let item = find_contract("ParserTest", &items);
1017        assert_eq!(
1018            item.natspec.as_ref().unwrap().items,
1019            vec![NatSpecItem {
1020                kind: NatSpecKind::Notice,
1021                comment: "A contract with correct natspec".to_string(),
1022                span: single_line_textrange(4..43)
1023            }]
1024        );
1025    }
1026
1027    #[test]
1028    fn test_parse_interface() {
1029        let cursor = parse_file(include_str!("../../test-data/InterfaceSample.sol"));
1030        let items = SlangParser::find_items(cursor);
1031        let item = find_interface("IInterfacedSample", &items);
1032        assert_eq!(
1033            item.natspec.as_ref().unwrap().items,
1034            vec![NatSpecItem {
1035                kind: NatSpecKind::Title,
1036                comment: "The interface".to_string(),
1037                span: single_line_textrange(4..24)
1038            }]
1039        );
1040    }
1041
1042    #[test]
1043    fn test_parse_library() {
1044        let cursor = parse_file(include_str!("../../test-data/LibrarySample.sol"));
1045        let items = SlangParser::find_items(cursor);
1046        let item = find_library("StringUtils", &items);
1047        assert_eq!(
1048            item.natspec.as_ref().unwrap().items,
1049            vec![NatSpecItem {
1050                kind: NatSpecKind::Title,
1051                comment: "StringUtils".to_string(),
1052                span: single_line_textrange(4..22)
1053            }]
1054        );
1055    }
1056
1057    #[test]
1058    fn test_parse_external_function() {
1059        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1060        let items = SlangParser::find_items(cursor);
1061        let item = find_function(
1062            "viewFunctionNoParams",
1063            Some(Parent::Contract("ParserTest")),
1064            &items,
1065        );
1066        assert_eq!(
1067            item.natspec.as_ref().unwrap().items,
1068            vec![
1069                NatSpecItem {
1070                    kind: NatSpecKind::Inheritdoc {
1071                        parent: INTERNER.get_or_intern("IParserTest")
1072                    },
1073                    comment: String::new(),
1074                    span: single_line_textrange(4..27)
1075                },
1076                NatSpecItem {
1077                    kind: NatSpecKind::Dev,
1078                    comment: "Dev comment for the function".to_string(),
1079                    span: single_line_textrange(4..37)
1080                }
1081            ]
1082        );
1083    }
1084
1085    #[test]
1086    fn test_parse_constant() {
1087        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1088        let items = SlangParser::find_items(cursor);
1089        let item = find_variable(
1090            "SOME_CONSTANT",
1091            Some(Parent::Contract("ParserTest")),
1092            &items,
1093        );
1094        assert_eq!(
1095            item.natspec.as_ref().unwrap().items,
1096            vec![NatSpecItem {
1097                kind: NatSpecKind::Inheritdoc {
1098                    parent: INTERNER.get_or_intern("IParserTest")
1099                },
1100                comment: String::new(),
1101                span: single_line_textrange(4..27)
1102            },]
1103        );
1104    }
1105
1106    #[test]
1107    fn test_parse_variable() {
1108        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1109        let items = SlangParser::find_items(cursor);
1110        let item = find_variable("someVariable", Some(Parent::Contract("ParserTest")), &items);
1111        assert_eq!(
1112            item.natspec.as_ref().unwrap().items,
1113            vec![NatSpecItem {
1114                kind: NatSpecKind::Inheritdoc {
1115                    parent: INTERNER.get_or_intern("IParserTest")
1116                },
1117                comment: String::new(),
1118                span: single_line_textrange(4..27)
1119            },]
1120        );
1121    }
1122
1123    #[test]
1124    fn test_parse_modifier() {
1125        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1126        let items = SlangParser::find_items(cursor);
1127        let item = find_modifier("someModifier", Some(Parent::Contract("ParserTest")), &items);
1128        assert_eq!(
1129            item.natspec.as_ref().unwrap().items,
1130            vec![
1131                NatSpecItem {
1132                    kind: NatSpecKind::Notice,
1133                    comment: "The description of the modifier".to_string(),
1134                    span: single_line_textrange(4..43)
1135                },
1136                NatSpecItem {
1137                    kind: NatSpecKind::Param {
1138                        name: INTERNER.get_or_intern("_param1")
1139                    },
1140                    comment: "The only parameter".to_string(),
1141                    span: single_line_textrange(4..37)
1142                },
1143            ]
1144        );
1145    }
1146
1147    #[test]
1148    fn test_parse_modifier_no_param() {
1149        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1150        let items = SlangParser::find_items(cursor);
1151        let item = find_modifier(
1152            "modifierWithoutParam",
1153            Some(Parent::Contract("ParserTest")),
1154            &items,
1155        );
1156        assert_eq!(
1157            item.natspec.as_ref().unwrap().items,
1158            vec![NatSpecItem {
1159                kind: NatSpecKind::Notice,
1160                comment: "The description of the modifier".to_string(),
1161                span: single_line_textrange(4..43)
1162            },]
1163        );
1164    }
1165
1166    #[test]
1167    fn test_parse_private_function() {
1168        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1169        let items = SlangParser::find_items(cursor);
1170        let item = find_function("_viewPrivate", Some(Parent::Contract("ParserTest")), &items);
1171        assert_eq!(
1172            item.natspec.as_ref().unwrap().items,
1173            vec![
1174                NatSpecItem {
1175                    kind: NatSpecKind::Notice,
1176                    comment: "Some private stuff".to_string(),
1177                    span: single_line_textrange(4..30)
1178                },
1179                NatSpecItem {
1180                    kind: NatSpecKind::Dev,
1181                    comment: "Dev comment for the private function".to_string(),
1182                    span: single_line_textrange(4..45)
1183                },
1184                NatSpecItem {
1185                    kind: NatSpecKind::Param {
1186                        name: INTERNER.get_or_intern("_paramName")
1187                    },
1188                    comment: "The parameter name".to_string(),
1189                    span: single_line_textrange(4..40)
1190                },
1191                NatSpecItem {
1192                    kind: NatSpecKind::Return {
1193                        name: Some(INTERNER.get_or_intern("_returned"))
1194                    },
1195                    comment: "The returned value".to_string(),
1196                    span: single_line_textrange(4..40)
1197                }
1198            ]
1199        );
1200    }
1201
1202    #[test]
1203    fn test_parse_multiline_descriptions() {
1204        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1205        let items = SlangParser::find_items(cursor);
1206        let item = find_function(
1207            "_viewMultiline",
1208            Some(Parent::Contract("ParserTest")),
1209            &items,
1210        );
1211        assert_eq!(
1212            item.natspec.as_ref().unwrap().items,
1213            vec![
1214                NatSpecItem {
1215                    kind: NatSpecKind::Notice,
1216                    comment: "Some internal stuff".to_string(),
1217                    span: single_line_textrange(4..31)
1218                },
1219                NatSpecItem {
1220                    kind: NatSpecKind::Notice,
1221                    comment: "Separate line".to_string(),
1222                    span: single_line_textrange(12..25)
1223                },
1224                NatSpecItem {
1225                    kind: NatSpecKind::Notice,
1226                    comment: "Third one".to_string(),
1227                    span: single_line_textrange(12..21)
1228                },
1229            ]
1230        );
1231    }
1232
1233    #[test]
1234    fn test_parse_multiple_same_tag() {
1235        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1236        let items = SlangParser::find_items(cursor);
1237        let item = find_function(
1238            "_viewDuplicateTag",
1239            Some(Parent::Contract("ParserTest")),
1240            &items,
1241        );
1242        assert_eq!(
1243            item.natspec.as_ref().unwrap().items,
1244            vec![
1245                NatSpecItem {
1246                    kind: NatSpecKind::Notice,
1247                    comment: "Some internal stuff".to_string(),
1248                    span: single_line_textrange(4..31)
1249                },
1250                NatSpecItem {
1251                    kind: NatSpecKind::Notice,
1252                    comment: "Separate line".to_string(),
1253                    span: single_line_textrange(4..25)
1254                },
1255            ]
1256        );
1257    }
1258
1259    #[test]
1260    fn test_parse_error() {
1261        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1262        let items = SlangParser::find_items(cursor);
1263        let item = find_error(
1264            "SimpleError",
1265            Some(Parent::Interface("IParserTest")),
1266            &items,
1267        );
1268        assert_eq!(
1269            item.natspec.as_ref().unwrap().items,
1270            vec![NatSpecItem {
1271                kind: NatSpecKind::Notice,
1272                comment: "Thrown whenever something goes wrong".to_string(),
1273                span: single_line_textrange(4..48)
1274            },]
1275        );
1276    }
1277
1278    #[test]
1279    fn test_parse_event() {
1280        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1281        let items = SlangParser::find_items(cursor);
1282        let item = find_event(
1283            "SimpleEvent",
1284            Some(Parent::Interface("IParserTest")),
1285            &items,
1286        );
1287        assert_eq!(
1288            item.natspec.as_ref().unwrap().items,
1289            vec![NatSpecItem {
1290                kind: NatSpecKind::Notice,
1291                comment: "Emitted whenever something happens".to_string(),
1292                span: single_line_textrange(4..46)
1293            },]
1294        );
1295    }
1296
1297    #[test]
1298    fn test_parse_struct() {
1299        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1300        let items = SlangParser::find_items(cursor);
1301        let item = find_struct(
1302            "SimplestStruct",
1303            Some(Parent::Interface("IParserTest")),
1304            &items,
1305        );
1306        assert_eq!(
1307            item.natspec.as_ref().unwrap().items,
1308            vec![
1309                NatSpecItem {
1310                    kind: NatSpecKind::Notice,
1311                    comment: "A struct holding 2 variables of type uint256".to_string(),
1312                    span: single_line_textrange(4..56)
1313                },
1314                NatSpecItem {
1315                    kind: NatSpecKind::Param {
1316                        name: INTERNER.get_or_intern("a")
1317                    },
1318                    comment: "The first variable".to_string(),
1319                    span: single_line_textrange(4..32)
1320                },
1321                NatSpecItem {
1322                    kind: NatSpecKind::Param {
1323                        name: INTERNER.get_or_intern("b")
1324                    },
1325                    comment: "The second variable".to_string(),
1326                    span: single_line_textrange(4..33)
1327                },
1328                NatSpecItem {
1329                    kind: NatSpecKind::Dev,
1330                    comment: "This is definitely a struct".to_string(),
1331                    span: single_line_textrange(4..36)
1332                },
1333            ]
1334        );
1335    }
1336
1337    #[test]
1338    fn test_parse_external_function_no_params() {
1339        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1340        let items = SlangParser::find_items(cursor);
1341        let item = find_function(
1342            "viewFunctionNoParams",
1343            Some(Parent::Interface("IParserTest")),
1344            &items,
1345        );
1346        assert_eq!(
1347            item.natspec.as_ref().unwrap().items,
1348            vec![
1349                NatSpecItem {
1350                    kind: NatSpecKind::Notice,
1351                    comment: "View function with no parameters".to_string(),
1352                    span: single_line_textrange(4..44)
1353                },
1354                NatSpecItem {
1355                    kind: NatSpecKind::Dev,
1356                    comment: "Natspec for the return value is missing".to_string(),
1357                    span: single_line_textrange(4..48)
1358                },
1359                NatSpecItem {
1360                    kind: NatSpecKind::Return { name: None },
1361                    comment: "The returned value".to_string(),
1362                    span: single_line_textrange(4..30)
1363                },
1364            ]
1365        );
1366    }
1367
1368    #[test]
1369    fn test_parse_external_function_params() {
1370        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1371        let items = SlangParser::find_items(cursor);
1372        let item = find_function(
1373            "viewFunctionWithParams",
1374            Some(Parent::Interface("IParserTest")),
1375            &items,
1376        );
1377        assert_eq!(
1378            item.natspec.as_ref().unwrap().items,
1379            vec![
1380                NatSpecItem {
1381                    kind: NatSpecKind::Notice,
1382                    comment: "A function with different style of natspec".to_string(),
1383                    span: TextIndex {
1384                        utf8: adjust_offset_windows!(9, 1),
1385                        line: 1,
1386                        col_utf8: 5,
1387                        col_utf16: 5,
1388                        col_utf32: 5,
1389                    }..TextIndex {
1390                        utf8: adjust_offset_windows!(59, 1),
1391                        line: 1,
1392                        col_utf8: 55,
1393                        col_utf16: 55,
1394                        col_utf32: 55,
1395                    }
1396                },
1397                NatSpecItem {
1398                    kind: NatSpecKind::Param {
1399                        name: INTERNER.get_or_intern("_param1")
1400                    },
1401                    comment: "The first parameter".to_string(),
1402                    span: TextIndex {
1403                        utf8: adjust_offset_windows!(65, 2),
1404                        line: 2,
1405                        col_utf8: 5,
1406                        col_utf16: 5,
1407                        col_utf32: 5,
1408                    }..TextIndex {
1409                        utf8: adjust_offset_windows!(100, 2),
1410                        line: 2,
1411                        col_utf8: 40,
1412                        col_utf16: 40,
1413                        col_utf32: 40,
1414                    }
1415                },
1416                NatSpecItem {
1417                    kind: NatSpecKind::Param {
1418                        name: INTERNER.get_or_intern("_param2")
1419                    },
1420                    comment: "The second parameter".to_string(),
1421                    span: TextIndex {
1422                        utf8: adjust_offset_windows!(106, 3),
1423                        line: 3,
1424                        col_utf8: 5,
1425                        col_utf16: 5,
1426                        col_utf32: 5,
1427                    }..TextIndex {
1428                        utf8: adjust_offset_windows!(142, 3),
1429                        line: 3,
1430                        col_utf8: 41,
1431                        col_utf16: 41,
1432                        col_utf32: 41,
1433                    }
1434                },
1435                NatSpecItem {
1436                    kind: NatSpecKind::Return { name: None },
1437                    comment: "The returned value".to_string(),
1438                    span: TextIndex {
1439                        utf8: adjust_offset_windows!(148, 4),
1440                        line: 4,
1441                        col_utf8: 5,
1442                        col_utf16: 5,
1443                        col_utf32: 5,
1444                    }..TextIndex {
1445                        utf8: adjust_offset_windows!(174, 4),
1446                        line: 4,
1447                        col_utf8: 31,
1448                        col_utf16: 31,
1449                        col_utf32: 31,
1450                    }
1451                },
1452            ]
1453        );
1454    }
1455
1456    #[test]
1457    fn test_parse_funny_struct() {
1458        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1459        let items = SlangParser::find_items(cursor);
1460        let item = find_struct(
1461            "SimpleStruct",
1462            Some(Parent::Contract("ParserTestFunny")),
1463            &items,
1464        );
1465        assert_eq!(item.natspec, None);
1466    }
1467
1468    #[test]
1469    fn test_parse_funny_variable() {
1470        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1471        let items = SlangParser::find_items(cursor);
1472        let item = find_variable(
1473            "someVariable",
1474            Some(Parent::Contract("ParserTestFunny")),
1475            &items,
1476        );
1477        assert_eq!(
1478            item.natspec.as_ref().unwrap().items,
1479            vec![
1480                NatSpecItem {
1481                    kind: NatSpecKind::Inheritdoc {
1482                        parent: INTERNER.get_or_intern("IParserTest")
1483                    },
1484                    comment: String::new(),
1485                    span: single_line_textrange(4..27)
1486                },
1487                NatSpecItem {
1488                    kind: NatSpecKind::Dev,
1489                    comment: "Providing context".to_string(),
1490                    span: single_line_textrange(4..26)
1491                }
1492            ]
1493        );
1494    }
1495
1496    #[test]
1497    fn test_parse_funny_constant() {
1498        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1499        let items = SlangParser::find_items(cursor);
1500        let item = find_variable(
1501            "SOME_CONSTANT",
1502            Some(Parent::Contract("ParserTestFunny")),
1503            &items,
1504        );
1505        assert_eq!(item.natspec, None);
1506    }
1507
1508    #[test]
1509    fn test_parse_funny_function_params() {
1510        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1511        let items = SlangParser::find_items(cursor);
1512        let item = find_function(
1513            "viewFunctionWithParams",
1514            Some(Parent::Contract("ParserTestFunny")),
1515            &items,
1516        );
1517        assert_eq!(item.natspec, None);
1518    }
1519
1520    #[test]
1521    fn test_parse_funny_function_private() {
1522        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1523        let items = SlangParser::find_items(cursor);
1524        let item = find_function(
1525            "_viewPrivateMulti",
1526            Some(Parent::Contract("ParserTestFunny")),
1527            &items,
1528        );
1529        assert_eq!(
1530            item.natspec.as_ref().unwrap().items,
1531            vec![
1532                NatSpecItem {
1533                    kind: NatSpecKind::Notice,
1534                    comment: "Some private stuff".to_string(),
1535                    span: TextIndex {
1536                        utf8: adjust_offset_windows!(9, 1),
1537                        line: 1,
1538                        col_utf8: 5,
1539                        col_utf16: 5,
1540                        col_utf32: 5,
1541                    }..TextIndex {
1542                        utf8: adjust_offset_windows!(37, 1),
1543                        line: 1,
1544                        col_utf8: 33,
1545                        col_utf16: 33,
1546                        col_utf32: 33,
1547                    }
1548                },
1549                NatSpecItem {
1550                    kind: NatSpecKind::Param {
1551                        name: INTERNER.get_or_intern("_paramName")
1552                    },
1553                    comment: "The parameter name".to_string(),
1554                    span: TextIndex {
1555                        utf8: adjust_offset_windows!(43, 2),
1556                        line: 2,
1557                        col_utf8: 5,
1558                        col_utf16: 5,
1559                        col_utf32: 5,
1560                    }..TextIndex {
1561                        utf8: adjust_offset_windows!(84, 2),
1562                        line: 2,
1563                        col_utf8: 46,
1564                        col_utf16: 46,
1565                        col_utf32: 46,
1566                    }
1567                },
1568                NatSpecItem {
1569                    kind: NatSpecKind::Return {
1570                        name: Some(INTERNER.get_or_intern("_returned"))
1571                    },
1572                    comment: "The returned value".to_string(),
1573                    span: TextIndex {
1574                        utf8: adjust_offset_windows!(90, 3),
1575                        line: 3,
1576                        col_utf8: 5,
1577                        col_utf16: 5,
1578                        col_utf32: 5,
1579                    }..TextIndex {
1580                        utf8: adjust_offset_windows!(134, 3),
1581                        line: 3,
1582                        col_utf8: 49,
1583                        col_utf16: 49,
1584                        col_utf32: 49,
1585                    }
1586                },
1587            ]
1588        );
1589    }
1590
1591    #[test]
1592    fn test_parse_funny_function_private_single() {
1593        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1594        let items = SlangParser::find_items(cursor);
1595        let item = find_function(
1596            "_viewPrivateSingle",
1597            Some(Parent::Contract("ParserTestFunny")),
1598            &items,
1599        );
1600        assert_eq!(
1601            item.natspec.as_ref().unwrap().items,
1602            vec![
1603                NatSpecItem {
1604                    kind: NatSpecKind::Notice,
1605                    comment: "Some private stuff".to_string(),
1606                    span: single_line_textrange(4..32)
1607                },
1608                NatSpecItem {
1609                    kind: NatSpecKind::Param {
1610                        name: INTERNER.get_or_intern("_paramName")
1611                    },
1612                    comment: "The parameter name".to_string(),
1613                    span: single_line_textrange(4..45)
1614                },
1615                NatSpecItem {
1616                    kind: NatSpecKind::Return {
1617                        name: Some(INTERNER.get_or_intern("_returned"))
1618                    },
1619                    comment: "The returned value".to_string(),
1620                    span: single_line_textrange(4..48)
1621                },
1622            ]
1623        );
1624    }
1625
1626    #[test]
1627    fn test_parse_funny_internal() {
1628        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1629        let items = SlangParser::find_items(cursor);
1630        let item = find_function(
1631            "_viewInternal",
1632            Some(Parent::Contract("ParserTestFunny")),
1633            &items,
1634        );
1635        assert_eq!(item.natspec, None);
1636    }
1637
1638    #[test]
1639    fn test_parse_funny_linter_fail() {
1640        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1641        let items = SlangParser::find_items(cursor);
1642        let item = find_function(
1643            "_viewLinterFail",
1644            Some(Parent::Contract("ParserTestFunny")),
1645            &items,
1646        );
1647        assert_eq!(
1648            item.natspec.as_ref().unwrap().items,
1649            vec![
1650                NatSpecItem {
1651                    kind: NatSpecKind::Notice,
1652                    comment: "Linter fail".to_string(),
1653                    span: single_line_textrange(4..23)
1654                },
1655                NatSpecItem {
1656                    kind: NatSpecKind::Dev,
1657                    comment: "What have I done".to_string(),
1658                    span: single_line_textrange(4..30)
1659                }
1660            ]
1661        );
1662    }
1663
1664    #[test]
1665    fn test_parse_funny_empty_return() {
1666        let cursor = parse_file(include_str!("../../test-data/ParserTest.sol"));
1667        let items = SlangParser::find_items(cursor);
1668        let item = find_function(
1669            "functionUnnamedEmptyReturn",
1670            Some(Parent::Contract("ParserTestFunny")),
1671            &items,
1672        );
1673        assert_eq!(
1674            item.natspec.as_ref().unwrap().items,
1675            vec![
1676                NatSpecItem {
1677                    kind: NatSpecKind::Notice,
1678                    comment: "fun fact: there are extra spaces after the 1st return".to_string(),
1679                    span: single_line_textrange(4..57)
1680                },
1681                NatSpecItem {
1682                    kind: NatSpecKind::Return { name: None },
1683                    comment: String::new(),
1684                    span: single_line_textrange(4..18)
1685                },
1686                NatSpecItem {
1687                    kind: NatSpecKind::Return { name: None },
1688                    comment: String::new(),
1689                    span: single_line_textrange(4..11)
1690                },
1691            ]
1692        );
1693    }
1694
1695    #[test]
1696    fn test_parse_solidity_latest() {
1697        let contents = include_str!("../../test-data/LatestVersion.sol");
1698        let solidity_version = detect_solidity_version(contents, PathBuf::new()).unwrap();
1699        let parser = Parser::create(solidity_version).unwrap();
1700        let output = parser.parse_file_contents(contents);
1701        assert!(output.is_valid(), "{:?}", output.errors());
1702    }
1703
1704    #[test]
1705    fn test_parse_solidity_unsupported() {
1706        let mut parser = SlangParser::builder().skip_version_detection(true).build();
1707        let file = File::open("test-data/UnsupportedVersion.sol").unwrap();
1708        let output = parser.parse_document(file, None::<PathBuf>, false);
1709        assert!(output.is_ok(), "{output:?}");
1710    }
1711
1712    #[test]
1713    fn test_parse_unicode() {
1714        let cursor = parse_file(include_str!("../../test-data/UnicodeSample.sol"));
1715        let items = SlangParser::find_items(cursor);
1716        let item = find_enum("TestEnum", Some(Parent::Contract("UnicodeSample")), &items);
1717        assert_eq!(
1718            item.natspec.as_ref().unwrap().items,
1719            vec![NatSpecItem {
1720                kind: NatSpecKind::Notice,
1721                comment: "An enum 👨🏾‍👩🏾‍👧🏾".to_string(),
1722                span: TextIndex {
1723                    utf8: 4,
1724                    line: 0,
1725                    col_utf8: 4,
1726                    col_utf16: 4,
1727                    col_utf32: 4,
1728                }..TextIndex {
1729                    utf8: 50,
1730                    line: 0,
1731                    col_utf8: 50,
1732                    col_utf16: 34,
1733                    col_utf32: 28,
1734                },
1735            }]
1736        );
1737    }
1738}