mago_docblock/
error.rs

1use serde::Deserialize;
2use serde::Serialize;
3
4use mago_span::HasSpan;
5use mago_span::Span;
6
7#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
8pub enum ParseError {
9    InvalidTrivia(Span),
10    UnclosedInlineTag(Span),
11    UnclosedInlineCode(Span),
12    UnclosedCodeBlock(Span),
13    InvalidTagName(Span),
14    InvalidAnnotationName(Span),
15    UnclosedAnnotationArguments(Span),
16    MalformedCodeBlock(Span),
17    InvalidComment(Span),
18    InconsistentIndentation(Span, usize, usize),
19    MissingAsterisk(Span),
20    MissingWhitespaceAfterAsterisk(Span),
21    MissingWhitespaceAfterOpeningAsterisk(Span),
22    MissingWhitespaceBeforeClosingAsterisk(Span),
23    ExpectedLine(Span),
24}
25
26impl HasSpan for ParseError {
27    fn span(&self) -> Span {
28        match self {
29            ParseError::InvalidTrivia(span)
30            | ParseError::UnclosedInlineTag(span)
31            | ParseError::UnclosedInlineCode(span)
32            | ParseError::UnclosedCodeBlock(span)
33            | ParseError::InvalidTagName(span)
34            | ParseError::InvalidAnnotationName(span)
35            | ParseError::UnclosedAnnotationArguments(span)
36            | ParseError::MalformedCodeBlock(span)
37            | ParseError::InvalidComment(span)
38            | ParseError::InconsistentIndentation(span, _, _)
39            | ParseError::MissingAsterisk(span)
40            | ParseError::MissingWhitespaceAfterAsterisk(span)
41            | ParseError::MissingWhitespaceAfterOpeningAsterisk(span)
42            | ParseError::MissingWhitespaceBeforeClosingAsterisk(span)
43            | ParseError::ExpectedLine(span) => *span,
44        }
45    }
46}
47
48impl std::error::Error for ParseError {}
49
50impl std::fmt::Display for ParseError {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match self {
53            ParseError::InvalidTrivia(_) | ParseError::InvalidComment(_) => {
54                write!(f, "Invalid docblock format")
55            }
56            ParseError::UnclosedInlineTag(_) => write!(f, "Unclosed inline tag"),
57            ParseError::UnclosedInlineCode(_) => write!(f, "Unclosed inline code"),
58            ParseError::UnclosedCodeBlock(_) => write!(f, "Unclosed code block"),
59            ParseError::InvalidTagName(_) => write!(f, "Invalid tag name"),
60            ParseError::InvalidAnnotationName(_) => write!(f, "Invalid annotation name"),
61            ParseError::UnclosedAnnotationArguments(_) => write!(f, "Unclosed annotation arguments"),
62            ParseError::MalformedCodeBlock(_) => write!(f, "Malformed code block"),
63            ParseError::InconsistentIndentation(_, expected, actual) => {
64                write!(f, "Inconsistent indentation: expected {expected}, found {actual}")
65            }
66            ParseError::MissingAsterisk(_) => write!(f, "Missing leading asterisk on docblock line"),
67            ParseError::MissingWhitespaceAfterAsterisk(_) => {
68                write!(f, "Missing space after leading asterisk")
69            }
70            ParseError::MissingWhitespaceAfterOpeningAsterisk(_)
71            | ParseError::MissingWhitespaceBeforeClosingAsterisk(_) => {
72                write!(f, "Improperly formatted single-line docblock")
73            }
74            ParseError::ExpectedLine(_) => write!(f, "Unexpected end of docblock"),
75        }
76    }
77}
78
79impl ParseError {
80    pub fn note(&self) -> String {
81        match self {
82            ParseError::InvalidTrivia(_) | ParseError::InvalidComment(_) => {
83                "Docblocks must start with `/**` and end with `*/`.".to_string()
84            }
85            ParseError::UnclosedInlineTag(_) => {
86                "Inline tags like `{@see}` must be closed with a matching `}`.".to_string()
87            }
88            ParseError::UnclosedInlineCode(_) => {
89                "Inline code snippets must be enclosed in matching backticks (`).".to_string()
90            }
91            ParseError::UnclosedCodeBlock(_) => {
92                "Multi-line code blocks must be terminated with a closing ```.".to_string()
93            }
94            ParseError::InvalidTagName(_) => {
95                "Docblock tags like `@param` must contain only letters, numbers, and hyphens.".to_string()
96            }
97            ParseError::InvalidAnnotationName(_) => {
98                "Annotations must start with an uppercase letter, `_`, or `\\`.".to_string()
99            }
100            ParseError::UnclosedAnnotationArguments(_) => {
101                "Arguments for an annotation must be enclosed in parentheses `()`.".to_string()
102            }
103            ParseError::MalformedCodeBlock(_) => {
104                "A code block must start with ``` optionally followed by a language identifier.".to_string()
105            }
106            ParseError::InconsistentIndentation(_, expected, actual) => {
107                format!(
108                    "This line has an indentation of {actual}, but {expected} was expected based on the first line."
109                )
110            }
111            ParseError::MissingAsterisk(_) => {
112                "Each line in a multi-line docblock should start with an aligned asterisk `*`.".to_string()
113            }
114            ParseError::MissingWhitespaceAfterAsterisk(_) => {
115                "A space is required after the leading `*` to separate it from the content.".to_string()
116            }
117            ParseError::MissingWhitespaceAfterOpeningAsterisk(_)
118            | ParseError::MissingWhitespaceBeforeClosingAsterisk(_) => {
119                "Single-line docblocks should have spaces padding the content, like `/** content */`.".to_string()
120            }
121            ParseError::ExpectedLine(_) => {
122                "A tag or description was expected here, but the docblock ended prematurely.".to_string()
123            }
124        }
125    }
126
127    pub fn help(&self) -> String {
128        match self {
129            ParseError::UnclosedInlineTag(_) => "Add a closing `}` to complete the inline tag.".to_string(),
130            ParseError::UnclosedInlineCode(_) => {
131                "Add a closing backtick ` ` ` to terminate the inline code.".to_string()
132            }
133            ParseError::UnclosedCodeBlock(_) => "Add a closing ``` to terminate the code block.".to_string(),
134            ParseError::InvalidTagName(_) => {
135                "Correct the tag name to use only valid characters (e.g., `@my-custom-tag`).".to_string()
136            }
137            ParseError::InvalidAnnotationName(_) => {
138                "Correct the annotation name to follow PSR-5 standards.".to_string()
139            }
140            ParseError::UnclosedAnnotationArguments(_) => {
141                "Add a closing `)` to complete the annotation's argument list.".to_string()
142            }
143            ParseError::InconsistentIndentation(_, _, _) => {
144                "Adjust the indentation to be consistent across all lines in the docblock.".to_string()
145            }
146            ParseError::MissingAsterisk(_) => "Add a leading `*` to the beginning of this line.".to_string(),
147            ParseError::MissingWhitespaceAfterAsterisk(_) => {
148                "Insert a space after the `*` at the beginning of the line.".to_string()
149            }
150            _ => "Review the docblock syntax to ensure it is correctly formatted.".to_string(),
151        }
152    }
153}