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