1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2024 Orange
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *          http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
use std::cmp;
19

            
20
use crate::ast::SourceInfo;
21
use crate::error;
22
use crate::error::DisplaySourceError;
23
use crate::reader::Pos;
24
use crate::text::{Style, StyledString};
25

            
26
/// Represents a parser error.
27
#[derive(Clone, Debug, PartialEq, Eq)]
28
pub struct ParseError {
29
    pub pos: Pos,
30
    pub recoverable: bool,
31
    pub kind: ParseErrorKind,
32
}
33

            
34
#[derive(Clone, Debug, PartialEq, Eq)]
35
pub enum ParseErrorKind {
36
    DuplicateSection,
37
    EscapeChar,
38
    Expecting { value: String },
39
    FileContentType,
40
    Filename,
41
    GraphQlVariables,
42
    HexDigit,
43
    InvalidCookieAttribute,
44
    InvalidDurationUnit(String),
45
    InvalidOption(String),
46
    Json(JsonErrorVariant),
47
    JsonPathExpr,
48
    Method { name: String },
49
    Multiline,
50
    MultilineAttribute(String),
51
    OddNumberOfHexDigits,
52
    Predicate,
53
    PredicateValue,
54
    RegexExpr { message: String },
55
    RequestSection,
56
    RequestSectionName { name: String },
57
    ResponseSection,
58
    ResponseSectionName { name: String },
59
    Space,
60
    Status,
61
    TemplateVariable,
62
    Unicode,
63
    UrlIllegalCharacter(char),
64
    UrlInvalidStart,
65
    Variable(String),
66
    Version,
67
    XPathExpr,
68
    Xml,
69
}
70

            
71
#[derive(Clone, Debug, PartialEq, Eq)]
72
pub enum JsonErrorVariant {
73
    TrailingComma,
74
    ExpectingElement,
75
    EmptyElement,
76
}
77

            
78
impl ParseError {
79
    /// Creates a new error for the position `pos`, of type `inner`.
80
8563720
    pub fn new(pos: Pos, recoverable: bool, kind: ParseErrorKind) -> ParseError {
81
8563720
        ParseError {
82
8563720
            pos,
83
8563720
            recoverable,
84
8563720
            kind,
85
        }
86
    }
87
}
88

            
89
impl DisplaySourceError for ParseError {
90
1100
    fn source_info(&self) -> SourceInfo {
91
1100
        SourceInfo {
92
1100
            start: self.pos,
93
1100
            end: self.pos,
94
        }
95
    }
96

            
97
425
    fn description(&self) -> String {
98
425
        match self.kind {
99
10
            ParseErrorKind::DuplicateSection => "Parsing section".to_string(),
100
10
            ParseErrorKind::EscapeChar => "Parsing escape character".to_string(),
101
90
            ParseErrorKind::Expecting { .. } => "Parsing literal".to_string(),
102
10
            ParseErrorKind::FileContentType => "Parsing file content type".to_string(),
103
20
            ParseErrorKind::Filename => "Parsing filename".to_string(),
104
            ParseErrorKind::GraphQlVariables => "Parsing GraphQL variables".to_string(),
105
10
            ParseErrorKind::HexDigit => "Parsing hexadecimal number".to_string(),
106
10
            ParseErrorKind::InvalidCookieAttribute => "Parsing cookie attribute".to_string(),
107
20
            ParseErrorKind::InvalidOption(_) => "Parsing option".to_string(),
108
10
            ParseErrorKind::InvalidDurationUnit(_) => "Parsing duration".to_string(),
109
60
            ParseErrorKind::Json(_) => "Parsing JSON".to_string(),
110
            ParseErrorKind::JsonPathExpr => "Parsing JSONPath expression".to_string(),
111
30
            ParseErrorKind::Method { .. } => "Parsing method".to_string(),
112
            ParseErrorKind::Multiline => "Parsing multiline".to_string(),
113
10
            ParseErrorKind::MultilineAttribute(..) => "Parsing multiline".to_string(),
114
            ParseErrorKind::OddNumberOfHexDigits => "Parsing hex bytearray".to_string(),
115
10
            ParseErrorKind::Predicate => "Parsing predicate".to_string(),
116
20
            ParseErrorKind::PredicateValue => "Parsing predicate value".to_string(),
117
10
            ParseErrorKind::RegexExpr { .. } => "Parsing regex".to_string(),
118
            ParseErrorKind::RequestSection => "Parsing section".to_string(),
119
20
            ParseErrorKind::RequestSectionName { .. } => "Parsing request section name".to_string(),
120
            ParseErrorKind::ResponseSection => "Parsing section".to_string(),
121
            ParseErrorKind::ResponseSectionName { .. } => {
122
                "Parsing response section name".to_string()
123
            }
124
15
            ParseErrorKind::Space => "Parsing space".to_string(),
125
10
            ParseErrorKind::Status => "Parsing status code".to_string(),
126
10
            ParseErrorKind::TemplateVariable => "Parsing template variable".to_string(),
127
10
            ParseErrorKind::Unicode => "Parsing unicode literal".to_string(),
128
            ParseErrorKind::UrlIllegalCharacter(_) => "Parsing URL".to_string(),
129
            ParseErrorKind::UrlInvalidStart => "Parsing URL".to_string(),
130
10
            ParseErrorKind::Variable(_) => "Parsing variable".to_string(),
131
10
            ParseErrorKind::Version => "Parsing version".to_string(),
132
            ParseErrorKind::XPathExpr => "Parsing XPath expression".to_string(),
133
10
            ParseErrorKind::Xml => "Parsing XML".to_string(),
134
        }
135
    }
136

            
137
220
    fn fixme(&self, content: &[&str]) -> StyledString {
138
220
        let message = match &self.kind {
139
5
            ParseErrorKind::DuplicateSection => "the section is already defined".to_string(),
140
5
            ParseErrorKind::EscapeChar => "the escaping sequence is not valid".to_string(),
141
45
            ParseErrorKind::Expecting { value } => format!("expecting '{value}'"),
142
5
            ParseErrorKind::FileContentType => "expecting a content type".to_string(),
143
10
            ParseErrorKind::Filename => "expecting a filename".to_string(),
144
            ParseErrorKind::GraphQlVariables => {
145
                "GraphQL variables is not a valid JSON object".to_string()
146
            }
147
5
            ParseErrorKind::HexDigit => "expecting a valid hexadecimal number".to_string(),
148
            ParseErrorKind::InvalidCookieAttribute => {
149
5
                "the cookie attribute is not valid".to_string()
150
            }
151
5
            ParseErrorKind::InvalidDurationUnit(name) => {
152
5
                let valid_values = ["ms", "s"];
153
5
                let default = format!("Valid values are {}", valid_values.join(", "));
154
5
                let did_you_mean = did_you_mean(&valid_values, name.as_str(), &default);
155
5
                format!("the duration unit is not valid. {did_you_mean}")
156
            }
157
10
            ParseErrorKind::InvalidOption(name) => {
158
10
                let valid_values = [
159
10
                    "aws-sigv4",
160
10
                    "cacert",
161
10
                    "cert",
162
10
                    "compressed",
163
10
                    "connect-to",
164
10
                    "delay",
165
10
                    "insecure",
166
10
                    "http1.0",
167
10
                    "http1.1",
168
10
                    "http2",
169
10
                    "http3",
170
10
                    "ipv4",
171
10
                    "ipv6",
172
10
                    "key",
173
10
                    "location",
174
10
                    "max-redirs",
175
10
                    "output",
176
10
                    "path-as-is",
177
10
                    "proxy",
178
10
                    "resolve",
179
10
                    "retry",
180
10
                    "retry-interval",
181
10
                    "skip",
182
10
                    "unix-socket",
183
10
                    "variable",
184
10
                    "verbose",
185
10
                    "very-verbose",
186
10
                ];
187
10
                let default = format!("Valid values are {}", valid_values.join(", "));
188
10
                let did_you_mean = did_you_mean(&valid_values, name.as_str(), &default);
189
10
                format!("the option name is not valid. {did_you_mean}")
190
            }
191
30
            ParseErrorKind::Json(variant) => match variant {
192
10
                JsonErrorVariant::TrailingComma => "trailing comma is not allowed".to_string(),
193
                JsonErrorVariant::EmptyElement => {
194
5
                    "expecting an element; found empty element instead".to_string()
195
                }
196
                JsonErrorVariant::ExpectingElement => {
197
15
                    "expecting a boolean, number, string, array, object or null".to_string()
198
                }
199
            },
200
            ParseErrorKind::JsonPathExpr => "expecting a JSONPath expression".to_string(),
201
20
            ParseErrorKind::Method { name } => {
202
20
                let valid_values = [
203
20
                    "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH",
204
20
                ];
205
20
                let default = format!("Valid values are {}", valid_values.join(", "));
206
20
                let did_you_mean = did_you_mean(&valid_values, name.as_str(), &default);
207
20
                format!("the HTTP method <{name}> is not valid. {did_you_mean}")
208
            }
209
            ParseErrorKind::Multiline => "the multiline is not valid".to_string(),
210
5
            ParseErrorKind::MultilineAttribute(name) => format!("Invalid attribute {name}"),
211
            ParseErrorKind::OddNumberOfHexDigits => {
212
                "expecting an even number of hex digits".to_string()
213
            }
214
5
            ParseErrorKind::Predicate => "expecting a predicate".to_string(),
215
10
            ParseErrorKind::PredicateValue => "invalid predicate value".to_string(),
216
5
            ParseErrorKind::RegexExpr { message } => format!("invalid Regex expression: {message}"),
217
            ParseErrorKind::RequestSection => {
218
                "this is not a valid section for a request".to_string()
219
            }
220
10
            ParseErrorKind::RequestSectionName { name } => {
221
10
                let valid_values = [
222
10
                    "QueryStringParams",
223
10
                    "FormParams",
224
10
                    "MultipartFormData",
225
10
                    "Cookies",
226
10
                    "Options",
227
10
                ];
228
10
                let default = format!("Valid values are {}", valid_values.join(", "));
229
10
                let did_you_mean = did_you_mean(&valid_values, name.as_str(), &default);
230
10
                format!("the section is not valid. {did_you_mean}")
231
            }
232
            ParseErrorKind::ResponseSection => {
233
                "this is not a valid section for a response".to_string()
234
            }
235
            ParseErrorKind::ResponseSectionName { name } => {
236
                let valid_values = ["Captures", "Asserts"];
237
                let default = "Valid values are Captures or Asserts";
238
                let did_your_mean = did_you_mean(&valid_values, name.as_str(), default);
239
                format!("the section is not valid. {did_your_mean}")
240
            }
241
10
            ParseErrorKind::Space => "expecting a space".to_string(),
242
5
            ParseErrorKind::Status => "HTTP status code is not valid".to_string(),
243
5
            ParseErrorKind::TemplateVariable => "expecting a variable".to_string(),
244
5
            ParseErrorKind::Unicode => "Invalid unicode literal".to_string(),
245
            ParseErrorKind::UrlIllegalCharacter(c) => format!("illegal character <{c}>"),
246
            ParseErrorKind::UrlInvalidStart => "expecting http://, https:// or {{".to_string(),
247
5
            ParseErrorKind::Variable(message) => message.clone(),
248
            ParseErrorKind::Version => {
249
5
                "HTTP version must be HTTP, HTTP/1.0, HTTP/1.1 or HTTP/2".to_string()
250
            }
251
            ParseErrorKind::XPathExpr => "expecting a XPath expression".to_string(),
252
5
            ParseErrorKind::Xml => "invalid XML".to_string(),
253
        };
254

            
255
220
        let message = error::add_carets(&message, self.source_info(), content);
256
220
        let mut s = StyledString::new();
257
220
        s.push_with(&message, Style::new().red().bold());
258
220
        s
259
    }
260
}
261

            
262
impl crate::combinator::ParseError for ParseError {
263
2061635
    fn is_recoverable(&self) -> bool {
264
2061635
        self.recoverable
265
    }
266

            
267
176810
    fn to_recoverable(self) -> Self {
268
176810
        ParseError {
269
176810
            recoverable: true,
270
176810
            ..self
271
        }
272
    }
273

            
274
135
    fn to_non_recoverable(self) -> Self {
275
135
        ParseError {
276
135
            recoverable: false,
277
135
            ..self
278
        }
279
    }
280
}
281

            
282
45
fn did_you_mean(valid_values: &[&str], actual: &str, default: &str) -> String {
283
45
    if let Some(suggest) = suggestion(valid_values, actual) {
284
15
        format!("Did you mean {suggest}?")
285
    } else {
286
30
        default.to_string()
287
    }
288
}
289

            
290
45
fn suggestion(valid_values: &[&str], actual: &str) -> Option<String> {
291
495
    for value in valid_values {
292
465
        if levenshtein_distance(
293
465
            value.to_lowercase().as_str(),
294
465
            actual.to_lowercase().as_str(),
295
465
        ) < 2
296
        {
297
15
            return Some(value.to_string());
298
        }
299
    }
300
30
    None
301
}
302

            
303
// From https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Rust
304
465
fn levenshtein_distance(s1: &str, s2: &str) -> usize {
305
465
    let v1: Vec<char> = s1.chars().collect();
306
465
    let v2: Vec<char> = s2.chars().collect();
307

            
308
20665
    fn min3<T: Ord>(v1: T, v2: T, v3: T) -> T {
309
20665
        cmp::min(v1, cmp::min(v2, v3))
310
    }
311
20665
    fn delta(x: char, y: char) -> usize {
312
20665
        usize::from(x != y)
313
    }
314

            
315
465
    let mut column: Vec<usize> = (0..=v1.len()).collect();
316
2810
    for x in 1..=v2.len() {
317
2810
        column[0] = x;
318
2810
        let mut lastdiag = x - 1;
319
20665
        for y in 1..=v1.len() {
320
20665
            let olddiag = column[y];
321
20665
            column[y] = min3(
322
20665
                column[y] + 1,
323
20665
                column[y - 1] + 1,
324
20665
                lastdiag + delta(v1[y - 1], v2[x - 1]),
325
20665
            );
326
20665
            lastdiag = olddiag;
327
        }
328
    }
329
465
    column[v1.len()]
330
}
331

            
332
#[cfg(test)]
333
mod tests {
334
    use super::*;
335
    use crate::error::OutputFormat;
336

            
337
    #[test]
338
    fn test_levenshtein() {
339
        assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
340
        assert_eq!(levenshtein_distance("Saturday", "Sunday"), 3);
341
    }
342

            
343
    #[test]
344
    fn test_suggestion() {
345
        let valid_values = ["Captures", "Asserts"];
346
        assert_eq!(
347
            suggestion(&valid_values, "Asserts"),
348
            Some("Asserts".to_string())
349
        );
350
        assert_eq!(
351
            suggestion(&valid_values, "Assert"),
352
            Some("Asserts".to_string())
353
        );
354
        assert_eq!(
355
            suggestion(&valid_values, "assert"),
356
            Some("Asserts".to_string())
357
        );
358
        assert_eq!(suggestion(&valid_values, "asser"), None);
359
    }
360

            
361
    #[test]
362
    fn test_parsing_error() {
363
        let content = "GET abc";
364
        let filename = "test.hurl";
365
        let error = ParseError {
366
            pos: Pos::new(1, 5),
367
            recoverable: false,
368
            kind: ParseErrorKind::UrlInvalidStart,
369
        };
370
        assert_eq!(
371
            error.to_string(filename, content, None, OutputFormat::Terminal(false)),
372
            r#"Parsing URL
373
  --> test.hurl:1:5
374
   |
375
 1 | GET abc
376
   |     ^ expecting http://, https:// or {{
377
   |"#
378
        );
379
    }
380
}