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