1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2026 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 hurl_core::ast::{
19
    Assert, Base64, Body, BooleanOption, Bytes, Capture, CertificateAttributeName, Comment, Cookie,
20
    CookiePath, CountOption, Duration, DurationOption, Entry, EntryOption, File, FilenameParam,
21
    FilenameValue, FilterValue, Hex, HurlFile, I64, IntegerValue, JsonValue, KeyValue,
22
    LineTerminator, Method, MultilineString, MultipartParam, NaturalOption, Number, OptionKind,
23
    Placeholder, Predicate, PredicateFuncValue, PredicateValue, Query, QueryValue, Regex,
24
    RegexValue, Request, Response, Section, SectionValue, StatusValue, Template, U64,
25
    VariableDefinition, VariableValue, VerbosityOption, VersionValue,
26
};
27
use hurl_core::types::{Count, DurationUnit, ToSource};
28

            
29
/// Lint a parsed `HurlFile` to a string.
30
84
pub fn lint_hurl_file(file: &HurlFile) -> String {
31
84
    file.lint()
32
}
33

            
34
/// Lint something (usually a Hurl AST node) to a string.
35
trait Lint {
36
    fn lint(&self) -> String;
37
}
38

            
39
impl Lint for Assert {
40
384
    fn lint(&self) -> String {
41
384
        let mut s = String::new();
42
384
        self.line_terminators
43
384
            .iter()
44
384
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
45
384
        s.push_str(&self.query.lint());
46
384
        if !self.filters.is_empty() {
47
117
            s.push(' ');
48
117
            let filters = self
49
117
                .filters
50
117
                .iter()
51
183
                .map(|(_, f)| f.value.lint())
52
117
                .collect::<Vec<_>>()
53
117
                .join(" ");
54
117
            s.push_str(&filters);
55
        }
56
384
        s.push(' ');
57
384
        s.push_str(&self.predicate.lint());
58
384
        s.push_str(&lint_lt(&self.line_terminator0, true));
59
384
        s
60
    }
61
}
62

            
63
impl Lint for Base64 {
64
15
    fn lint(&self) -> String {
65
15
        let mut s = String::new();
66
15
        s.push_str("base64,");
67
15
        s.push_str(self.source.as_str());
68
15
        s.push(';');
69
15
        s
70
    }
71
}
72

            
73
impl Lint for Body {
74
108
    fn lint(&self) -> String {
75
108
        let mut s = String::new();
76
108
        self.line_terminators
77
108
            .iter()
78
114
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
79
108
        s.push_str(&self.value.lint());
80
108
        s.push_str(&lint_lt(&self.line_terminator0, true));
81
108
        s
82
    }
83
}
84

            
85
impl Lint for BooleanOption {
86
144
    fn lint(&self) -> String {
87
144
        match self {
88
84
            BooleanOption::Literal(value) => value.to_string(),
89
60
            BooleanOption::Placeholder(value) => value.lint(),
90
        }
91
    }
92
}
93

            
94
impl Lint for Bytes {
95
108
    fn lint(&self) -> String {
96
108
        match self {
97
6
            Bytes::Json(value) => value.lint(),
98
3
            Bytes::Xml(value) => value.clone(),
99
51
            Bytes::MultilineString(value) => value.lint(),
100
15
            Bytes::OnelineString(value) => value.lint(),
101
12
            Bytes::Base64(value) => value.lint(),
102
12
            Bytes::File(value) => value.lint(),
103
9
            Bytes::Hex(value) => value.lint(),
104
        }
105
    }
106
}
107

            
108
impl Lint for Capture {
109
18
    fn lint(&self) -> String {
110
18
        let mut s = String::new();
111
18
        self.line_terminators
112
18
            .iter()
113
18
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
114
18
        s.push_str(&self.name.lint());
115
18
        s.push(':');
116
18
        s.push(' ');
117
18
        s.push_str(&self.query.lint());
118
18
        if !self.filters.is_empty() {
119
3
            s.push(' ');
120
3
            let filters = self
121
3
                .filters
122
3
                .iter()
123
4
                .map(|(_, f)| f.value.lint())
124
3
                .collect::<Vec<_>>()
125
3
                .join(" ");
126
3
            s.push_str(&filters);
127
        }
128
18
        if self.redacted {
129
6
            s.push(' ');
130
6
            s.push_str("redact");
131
        }
132
18
        s.push_str(&lint_lt(&self.line_terminator0, true));
133
18
        s
134
    }
135
}
136

            
137
impl Lint for CertificateAttributeName {
138
33
    fn lint(&self) -> String {
139
33
        self.to_source().to_string()
140
    }
141
}
142

            
143
impl Lint for Cookie {
144
9
    fn lint(&self) -> String {
145
9
        let mut s = String::new();
146
9
        self.line_terminators
147
9
            .iter()
148
9
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
149
9
        s.push_str(&self.name.lint());
150
9
        s.push(':');
151
9
        s.push(' ');
152
9
        s.push_str(&self.value.lint());
153
9
        s.push_str(&lint_lt(&self.line_terminator0, true));
154
9
        s
155
    }
156
}
157

            
158
impl Lint for CookiePath {
159
9
    fn lint(&self) -> String {
160
9
        self.to_source().to_string()
161
    }
162
}
163

            
164
impl Lint for Comment {
165
318
    fn lint(&self) -> String {
166
318
        format!("#{}", self.value.trim_end())
167
    }
168
}
169

            
170
impl Lint for Count {
171
21
    fn lint(&self) -> String {
172
21
        self.to_string()
173
    }
174
}
175

            
176
impl Lint for CountOption {
177
30
    fn lint(&self) -> String {
178
30
        match self {
179
21
            CountOption::Literal(value) => value.lint(),
180
9
            CountOption::Placeholder(value) => value.lint(),
181
        }
182
    }
183
}
184

            
185
impl Lint for Entry {
186
285
    fn lint(&self) -> String {
187
285
        let mut s = String::new();
188
285
        s.push_str(&self.request.lint());
189
285
        if let Some(response) = &self.response {
190
114
            s.push_str(&response.lint());
191
        }
192
285
        s
193
    }
194
}
195

            
196
impl Lint for EntryOption {
197
348
    fn lint(&self) -> String {
198
348
        let mut s = String::new();
199
348
        self.line_terminators
200
348
            .iter()
201
349
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
202
348
        s.push_str(&self.kind.lint());
203
348
        s.push_str(&lint_lt(&self.line_terminator0, true));
204
348
        s
205
    }
206
}
207

            
208
impl Lint for File {
209
15
    fn lint(&self) -> String {
210
15
        let mut s = String::new();
211
15
        s.push_str("file,");
212
15
        s.push_str(&self.filename.lint());
213
15
        s.push(';');
214
15
        s
215
    }
216
}
217

            
218
impl Lint for FilenameParam {
219
6
    fn lint(&self) -> String {
220
6
        let mut s = String::new();
221
6
        self.line_terminators
222
6
            .iter()
223
6
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
224
6
        s.push_str(&self.key.lint());
225
6
        s.push(':');
226
6
        s.push(' ');
227
6
        s.push_str(&self.value.lint());
228
6
        s.push_str(&lint_lt(&self.line_terminator0, true));
229
6
        s
230
    }
231
}
232

            
233
impl Lint for FilenameValue {
234
6
    fn lint(&self) -> String {
235
6
        let mut s = String::new();
236
6
        s.push_str("file,");
237
6
        s.push_str(&self.filename.lint());
238
6
        s.push(';');
239
6
        if let Some(content_type) = &self.content_type {
240
3
            s.push(' ');
241
3
            s.push_str(&content_type.lint());
242
        }
243
6
        s
244
    }
245
}
246

            
247
impl Lint for FilterValue {
248
147
    fn lint(&self) -> String {
249
147
        let mut s = String::new();
250
147
        s.push_str(self.identifier());
251
147
        match self {
252
6
            FilterValue::CharsetDecode { encoding, .. } => {
253
6
                s.push(' ');
254
6
                s.push_str(&encoding.lint());
255
            }
256
3
            FilterValue::Decode { encoding, .. } => {
257
3
                s.push(' ');
258
3
                s.push_str(&encoding.lint());
259
            }
260
6
            FilterValue::Format { fmt, .. } => {
261
6
                s.push(' ');
262
6
                s.push_str(&fmt.lint());
263
            }
264
9
            FilterValue::DateFormat { fmt, .. } => {
265
9
                s.push(' ');
266
9
                s.push_str(&fmt.lint());
267
            }
268
9
            FilterValue::JsonPath { expr, .. } => {
269
9
                s.push(' ');
270
9
                s.push_str(&expr.lint());
271
            }
272
3
            FilterValue::Nth { n, .. } => {
273
3
                s.push(' ');
274
3
                s.push_str(&n.lint());
275
            }
276
3
            FilterValue::Regex { value, .. } => {
277
3
                s.push(' ');
278
3
                s.push_str(&value.lint());
279
            }
280
            FilterValue::Replace {
281
15
                old_value,
282
15
                new_value,
283
                ..
284
15
            } => {
285
15
                s.push(' ');
286
15
                s.push_str(&old_value.lint());
287
15
                s.push(' ');
288
15
                s.push_str(&new_value.lint());
289
            }
290
3
            FilterValue::Split { sep, .. } => {
291
3
                s.push(' ');
292
3
                s.push_str(&sep.lint());
293
            }
294
            FilterValue::ReplaceRegex {
295
3
                pattern, new_value, ..
296
3
            } => {
297
3
                s.push(' ');
298
3
                s.push_str(&pattern.lint());
299
3
                s.push(' ');
300
3
                s.push_str(&new_value.lint());
301
            }
302
3
            FilterValue::ToDate { fmt, .. } => {
303
3
                s.push(' ');
304
3
                s.push_str(&fmt.lint());
305
            }
306
3
            FilterValue::UrlQueryParam { param, .. } => {
307
3
                s.push(' ');
308
3
                s.push_str(&param.lint());
309
            }
310
3
            FilterValue::XPath { expr, .. } => {
311
3
                s.push(' ');
312
3
                s.push_str(&expr.lint());
313
            }
314
            FilterValue::Base64Decode
315
            | FilterValue::Base64Encode
316
            | FilterValue::Base64UrlSafeDecode
317
            | FilterValue::Base64UrlSafeEncode
318
            | FilterValue::Count
319
            | FilterValue::DaysAfterNow
320
            | FilterValue::DaysBeforeNow
321
            | FilterValue::First
322
            | FilterValue::HtmlEscape
323
            | FilterValue::HtmlUnescape
324
            | FilterValue::Last
325
            | FilterValue::Location
326
            | FilterValue::ToFloat
327
            | FilterValue::ToHex
328
            | FilterValue::ToInt
329
            | FilterValue::ToString
330
            | FilterValue::UrlDecode
331
            | FilterValue::UrlEncode
332
            | FilterValue::Utf8Decode
333
78
            | FilterValue::Utf8Encode => {}
334
        }
335
147
        s
336
    }
337
}
338

            
339
impl Lint for Hex {
340
36
    fn lint(&self) -> String {
341
36
        let mut s = String::new();
342
36
        s.push_str("hex,");
343
36
        s.push_str(self.source.as_str());
344
36
        s.push(';');
345
36
        s
346
    }
347
}
348

            
349
impl Lint for HurlFile {
350
84
    fn lint(&self) -> String {
351
84
        let mut s = String::new();
352
313
        self.entries.iter().for_each(|e| s.push_str(&e.lint()));
353
84
        self.line_terminators
354
84
            .iter()
355
92
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
356
84
        s
357
    }
358
}
359

            
360
impl Lint for IntegerValue {
361
3
    fn lint(&self) -> String {
362
3
        match self {
363
3
            IntegerValue::Literal(value) => value.lint(),
364
            IntegerValue::Placeholder(value) => value.lint(),
365
        }
366
    }
367
}
368

            
369
impl Lint for I64 {
370
3
    fn lint(&self) -> String {
371
3
        self.to_source().to_string()
372
    }
373
}
374

            
375
impl Lint for JsonValue {
376
6
    fn lint(&self) -> String {
377
6
        self.to_source().to_string()
378
    }
379
}
380

            
381
impl Lint for KeyValue {
382
132
    fn lint(&self) -> String {
383
132
        let mut s = String::new();
384
132
        self.line_terminators
385
132
            .iter()
386
133
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
387
132
        s.push_str(&self.key.lint());
388
132
        s.push(':');
389
132
        if !self.value.is_empty() {
390
126
            s.push(' ');
391
126
            s.push_str(&self.value.lint());
392
        }
393
132
        s.push_str(&lint_lt(&self.line_terminator0, true));
394
132
        s
395
    }
396
}
397

            
398
1818
fn lint_lt(lt: &LineTerminator, is_trailing: bool) -> String {
399
1818
    let mut s = String::new();
400
1818
    if let Some(comment) = &lt.comment {
401
318
        if is_trailing {
402
231
            // if line terminator is a trailing terminator, we keep the leading whitespaces
403
231
            // to keep user alignment.
404
231
            s.push_str(lt.space0.as_str());
405
        }
406
318
        s.push_str(&comment.lint());
407
1500
    };
408
    // We always terminate a file by a newline
409
1818
    if lt.newline.value.is_empty() {
410
3
        s.push('\n');
411
1815
    } else {
412
1815
        s.push_str(&lt.newline.value);
413
    }
414
1818
    s
415
}
416

            
417
impl Lint for Method {
418
285
    fn lint(&self) -> String {
419
285
        self.to_source().to_string()
420
    }
421
}
422

            
423
impl Lint for MultipartParam {
424
9
    fn lint(&self) -> String {
425
9
        let mut s = String::new();
426
9
        match self {
427
3
            MultipartParam::Param(param) => s.push_str(&param.lint()),
428
6
            MultipartParam::FilenameParam(param) => s.push_str(&param.lint()),
429
        }
430
9
        s
431
    }
432
}
433

            
434
impl Lint for MultilineString {
435
69
    fn lint(&self) -> String {
436
69
        self.to_source().to_string()
437
    }
438
}
439

            
440
impl Lint for NaturalOption {
441
6
    fn lint(&self) -> String {
442
6
        match self {
443
3
            NaturalOption::Literal(value) => value.lint(),
444
3
            NaturalOption::Placeholder(value) => value.lint(),
445
        }
446
    }
447
}
448

            
449
impl Lint for Number {
450
108
    fn lint(&self) -> String {
451
108
        self.to_source().to_string()
452
    }
453
}
454

            
455
impl Lint for OptionKind {
456
348
    fn lint(&self) -> String {
457
348
        let mut s = String::new();
458
348
        s.push_str(self.identifier());
459
348
        s.push(':');
460
348
        s.push(' ');
461
348
        let value = match self {
462
6
            OptionKind::AwsSigV4(value) => value.lint(),
463
6
            OptionKind::CaCertificate(value) => value.lint(),
464
9
            OptionKind::ClientCert(value) => value.lint(),
465
6
            OptionKind::ClientKey(value) => value.lint(),
466
6
            OptionKind::Compressed(value) => value.lint(),
467
6
            OptionKind::ConnectTo(value) => value.lint(),
468
6
            OptionKind::ConnectTimeout(value) => {
469
6
                lint_duration_option(value, DurationUnit::MilliSecond)
470
            }
471
18
            OptionKind::Delay(value) => lint_duration_option(value, DurationUnit::MilliSecond),
472
6
            OptionKind::Digest(value) => value.lint(),
473
6
            OptionKind::FailWithBody(value) => value.lint(),
474
6
            OptionKind::Header(value) => value.lint(),
475
6
            OptionKind::Http10(value) => value.lint(),
476
6
            OptionKind::Http11(value) => value.lint(),
477
6
            OptionKind::Http2(value) => value.lint(),
478
3
            OptionKind::Http2PriorKnowledge(value) => value.lint(),
479
6
            OptionKind::Http3(value) => value.lint(),
480
9
            OptionKind::Insecure(value) => value.lint(),
481
6
            OptionKind::IpV4(value) => value.lint(),
482
6
            OptionKind::IpV6(value) => value.lint(),
483
9
            OptionKind::FollowLocation(value) => value.lint(),
484
6
            OptionKind::FollowLocationTrusted(value) => value.lint(),
485
6
            OptionKind::LimitRate(value) => value.lint(),
486
6
            OptionKind::MaxRedirect(value) => value.lint(),
487
6
            OptionKind::MaxTime(value) => lint_duration_option(value, DurationUnit::MilliSecond),
488
9
            OptionKind::Negotiate(value) => value.lint(),
489
6
            OptionKind::NetRc(value) => value.lint(),
490
6
            OptionKind::NetRcFile(value) => value.lint(),
491
6
            OptionKind::NetRcOptional(value) => value.lint(),
492
6
            OptionKind::NoHeader(value) => value.lint(),
493
9
            OptionKind::Ntlm(value) => value.lint(),
494
6
            OptionKind::Output(value) => value.lint(),
495
6
            OptionKind::PathAsIs(value) => value.lint(),
496
6
            OptionKind::PinnedPublicKey(value) => value.lint(),
497
6
            OptionKind::Proxy(value) => value.lint(),
498
9
            OptionKind::Repeat(value) => value.lint(),
499
6
            OptionKind::Resolve(value) => value.lint(),
500
15
            OptionKind::Retry(value) => value.lint(),
501
12
            OptionKind::RetryInterval(value) => {
502
12
                lint_duration_option(value, DurationUnit::MilliSecond)
503
            }
504
6
            OptionKind::Skip(value) => value.lint(),
505
6
            OptionKind::UnixSocket(value) => value.lint(),
506
12
            OptionKind::User(value) => value.lint(),
507
27
            OptionKind::Variable(value) => value.lint(),
508
15
            OptionKind::Verbose(value) => value.lint(),
509
6
            OptionKind::Verbosity(value) => value.lint(),
510
6
            OptionKind::VeryVerbose(value) => value.lint(),
511
        };
512
348
        s.push_str(&value);
513
348
        s
514
    }
515
}
516

            
517
impl Lint for Query {
518
402
    fn lint(&self) -> String {
519
402
        let mut s = String::new();
520
402
        s.push_str(self.value.identifier());
521
402
        match &self.value {
522
6
            QueryValue::Status => {}
523
3
            QueryValue::Version => {}
524
3
            QueryValue::Url => {}
525
12
            QueryValue::Header { name, .. } => {
526
12
                s.push(' ');
527
12
                s.push_str(&name.lint());
528
            }
529
9
            QueryValue::Cookie { expr, .. } => {
530
9
                s.push(' ');
531
9
                s.push_str(&expr.lint());
532
            }
533
33
            QueryValue::Body => {}
534
3
            QueryValue::Xpath { expr, .. } => {
535
3
                s.push(' ');
536
3
                s.push_str(&expr.lint());
537
            }
538
243
            QueryValue::Jsonpath { expr, .. } => {
539
243
                s.push(' ');
540
243
                s.push_str(&expr.lint());
541
            }
542
3
            QueryValue::Regex { value, .. } => {
543
3
                s.push(' ');
544
3
                s.push_str(&value.lint());
545
            }
546
9
            QueryValue::Variable { name, .. } => {
547
9
                s.push(' ');
548
9
                s.push_str(&name.lint());
549
            }
550
3
            QueryValue::Duration => {}
551
27
            QueryValue::Bytes => {}
552
            QueryValue::RawBytes => {}
553
6
            QueryValue::Sha256 => {}
554
3
            QueryValue::Md5 => {}
555
33
            QueryValue::Certificate { attribute_name, .. } => {
556
33
                s.push(' ');
557
33
                s.push_str(&attribute_name.lint());
558
            }
559
6
            QueryValue::Ip => {}
560
            QueryValue::Redirects => {}
561
        }
562
402
        s
563
    }
564
}
565

            
566
impl Lint for Placeholder {
567
87
    fn lint(&self) -> String {
568
87
        self.to_source().to_string()
569
    }
570
}
571

            
572
impl Lint for Predicate {
573
384
    fn lint(&self) -> String {
574
384
        let mut s = String::new();
575
384
        if self.not {
576
3
            s.push_str("not");
577
3
            s.push(' ');
578
        }
579
384
        s.push_str(&self.predicate_func.value.lint());
580
384
        s
581
    }
582
}
583

            
584
impl Lint for PredicateFuncValue {
585
384
    fn lint(&self) -> String {
586
384
        let mut s = String::new();
587
384
        s.push_str(self.identifier());
588
384
        match self {
589
252
            PredicateFuncValue::Equal { value, .. } => {
590
252
                s.push(' ');
591
252
                s.push_str(&value.lint());
592
            }
593
9
            PredicateFuncValue::NotEqual { value, .. } => {
594
9
                s.push(' ');
595
9
                s.push_str(&value.lint());
596
            }
597
9
            PredicateFuncValue::GreaterThan { value, .. } => {
598
9
                s.push(' ');
599
9
                s.push_str(&value.lint());
600
            }
601
3
            PredicateFuncValue::GreaterThanOrEqual { value, .. } => {
602
3
                s.push(' ');
603
3
                s.push_str(&value.lint());
604
            }
605
12
            PredicateFuncValue::LessThan { value, .. } => {
606
12
                s.push(' ');
607
12
                s.push_str(&value.lint());
608
            }
609
3
            PredicateFuncValue::LessThanOrEqual { value, .. } => {
610
3
                s.push(' ');
611
3
                s.push_str(&value.lint());
612
            }
613
9
            PredicateFuncValue::StartWith { value, .. } => {
614
9
                s.push(' ');
615
9
                s.push_str(&value.lint());
616
            }
617
6
            PredicateFuncValue::EndWith { value, .. } => {
618
6
                s.push(' ');
619
6
                s.push_str(&value.lint());
620
            }
621
9
            PredicateFuncValue::Contain { value, .. } => {
622
9
                s.push(' ');
623
9
                s.push_str(&value.lint());
624
            }
625
3
            PredicateFuncValue::Include { value, .. } => {
626
3
                s.push(' ');
627
3
                s.push_str(&value.lint());
628
            }
629
9
            PredicateFuncValue::Match { value, .. } => {
630
9
                s.push(' ');
631
9
                s.push_str(&value.lint());
632
            }
633
            PredicateFuncValue::Exist
634
            | PredicateFuncValue::IsBoolean
635
            | PredicateFuncValue::IsCollection
636
            | PredicateFuncValue::IsDate
637
            | PredicateFuncValue::IsEmpty
638
            | PredicateFuncValue::IsFloat
639
            | PredicateFuncValue::IsInteger
640
            | PredicateFuncValue::IsIpv4
641
            | PredicateFuncValue::IsIpv6
642
            | PredicateFuncValue::IsIsoDate
643
            | PredicateFuncValue::IsList
644
            | PredicateFuncValue::IsNumber
645
            | PredicateFuncValue::IsObject
646
            | PredicateFuncValue::IsString
647
60
            | PredicateFuncValue::IsUuid => {}
648
        }
649
384
        s
650
    }
651
}
652

            
653
impl Lint for PredicateValue {
654
324
    fn lint(&self) -> String {
655
324
        match self {
656
3
            PredicateValue::Base64(value) => value.lint(),
657
3
            PredicateValue::Bool(value) => value.to_string(),
658
3
            PredicateValue::File(value) => value.lint(),
659
27
            PredicateValue::Hex(value) => value.lint(),
660
18
            PredicateValue::MultilineString(value) => value.lint(),
661
3
            PredicateValue::Null => "null".to_string(),
662
108
            PredicateValue::Number(value) => value.lint(),
663
3
            PredicateValue::Placeholder(value) => value.lint(),
664
6
            PredicateValue::Regex(value) => value.lint(),
665
150
            PredicateValue::String(value) => value.lint(),
666
        }
667
    }
668
}
669

            
670
impl Lint for Regex {
671
12
    fn lint(&self) -> String {
672
12
        self.to_source().to_string()
673
    }
674
}
675

            
676
impl Lint for RegexValue {
677
9
    fn lint(&self) -> String {
678
9
        match self {
679
3
            RegexValue::Template(value) => value.lint(),
680
6
            RegexValue::Regex(value) => value.lint(),
681
        }
682
    }
683
}
684

            
685
impl Lint for Request {
686
285
    fn lint(&self) -> String {
687
285
        let mut s = String::new();
688
285
        self.line_terminators
689
285
            .iter()
690
348
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
691
285
        s.push_str(&self.method.lint());
692
285
        s.push(' ');
693
285
        s.push_str(&self.url.lint());
694
285
        s.push_str(&lint_lt(&self.line_terminator0, true));
695

            
696
312
        self.headers.iter().for_each(|h| s.push_str(&h.lint()));
697

            
698
        // We rewrite our file and reorder the various section.
699
285
        if let Some(section) = get_option_section(self) {
700
42
            s.push_str(&section.lint());
701
        }
702
285
        if let Some(section) = get_query_params_section(self) {
703
12
            s.push_str(&section.lint());
704
        }
705
285
        if let Some(section) = get_basic_auth_section(self) {
706
3
            s.push_str(&section.lint());
707
        }
708
285
        if let Some(section) = get_form_params_section(self) {
709
6
            s.push_str(&section.lint());
710
        }
711
285
        if let Some(section) = get_multipart_section(self) {
712
6
            s.push_str(&section.lint());
713
        }
714
285
        if let Some(section) = get_cookies_section(self) {
715
9
            s.push_str(&section.lint());
716
        }
717
285
        if let Some(body) = &self.body {
718
60
            s.push_str(&body.lint());
719
        }
720
285
        s
721
    }
722
}
723

            
724
impl Lint for Response {
725
114
    fn lint(&self) -> String {
726
114
        let mut s = String::new();
727
114
        self.line_terminators
728
114
            .iter()
729
115
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
730
114
        s.push_str(&self.version.value.lint());
731
114
        s.push(' ');
732
114
        s.push_str(&self.status.value.lint());
733
114
        s.push_str(&lint_lt(&self.line_terminator0, true));
734

            
735
119
        self.headers.iter().for_each(|h| s.push_str(&h.lint()));
736

            
737
114
        if let Some(section) = get_captures_section(self) {
738
12
            s.push_str(&section.lint());
739
        }
740
114
        if let Some(section) = get_asserts_section(self) {
741
48
            s.push_str(&section.lint());
742
        }
743
114
        if let Some(body) = &self.body {
744
48
            s.push_str(&body.lint());
745
        }
746
114
        s
747
    }
748
}
749

            
750
impl Lint for Section {
751
138
    fn lint(&self) -> String {
752
138
        let mut s = String::new();
753
138
        self.line_terminators
754
138
            .iter()
755
150
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
756
138
        s.push('[');
757
138
        s.push_str(self.identifier());
758
138
        s.push(']');
759
138
        s.push_str(&lint_lt(&self.line_terminator0, true));
760
138
        s.push_str(&self.value.lint());
761
138
        s
762
    }
763
}
764

            
765
impl Lint for SectionValue {
766
138
    fn lint(&self) -> String {
767
138
        let mut s = String::new();
768
3
        match self {
769
12
            SectionValue::QueryParams(params, _) => {
770
22
                params.iter().for_each(|p| s.push_str(&p.lint()));
771
            }
772
3
            SectionValue::BasicAuth(Some(auth)) => {
773
3
                s.push_str(&auth.lint());
774
            }
775
            SectionValue::BasicAuth(_) => {}
776
6
            SectionValue::FormParams(params, _) => {
777
14
                params.iter().for_each(|p| s.push_str(&p.lint()));
778
            }
779
6
            SectionValue::MultipartFormData(params, _) => {
780
11
                params.iter().for_each(|p| s.push_str(&p.lint()));
781
            }
782
9
            SectionValue::Cookies(cookies) => {
783
12
                cookies.iter().for_each(|c| s.push_str(&c.lint()));
784
            }
785
12
            SectionValue::Captures(captures) => {
786
22
                captures.iter().for_each(|c| s.push_str(&c.lint()));
787
            }
788
48
            SectionValue::Asserts(asserts) => {
789
400
                asserts.iter().for_each(|a| s.push_str(&a.lint()));
790
            }
791
42
            SectionValue::Options(options) => {
792
362
                options.iter().for_each(|o| s.push_str(&o.lint()));
793
            }
794
        }
795
138
        s
796
    }
797
}
798

            
799
impl Lint for StatusValue {
800
114
    fn lint(&self) -> String {
801
114
        self.to_source().to_string()
802
    }
803
}
804

            
805
impl Lint for Template {
806
1215
    fn lint(&self) -> String {
807
1215
        self.to_source().to_string()
808
    }
809
}
810

            
811
impl Lint for U64 {
812
33
    fn lint(&self) -> String {
813
33
        self.to_source().to_string()
814
    }
815
}
816

            
817
impl Lint for VariableDefinition {
818
27
    fn lint(&self) -> String {
819
27
        let mut s = String::new();
820
27
        s.push_str(&self.name);
821
27
        s.push('=');
822
27
        s.push_str(&self.value.lint());
823
27
        s
824
    }
825
}
826

            
827
impl Lint for VariableValue {
828
27
    fn lint(&self) -> String {
829
27
        self.to_source().to_string()
830
    }
831
}
832

            
833
impl Lint for VersionValue {
834
114
    fn lint(&self) -> String {
835
114
        self.to_source().to_string()
836
    }
837
}
838

            
839
impl Lint for VerbosityOption {
840
6
    fn lint(&self) -> String {
841
6
        self.to_string()
842
    }
843
}
844

            
845
114
fn get_asserts_section(response: &Response) -> Option<&Section> {
846
114
    for s in &response.sections {
847
60
        if let SectionValue::Asserts(_) = s.value {
848
48
            return Some(s);
849
        }
850
    }
851
66
    None
852
}
853

            
854
114
fn get_captures_section(response: &Response) -> Option<&Section> {
855
114
    for s in &response.sections {
856
51
        if let SectionValue::Captures(_) = s.value {
857
12
            return Some(s);
858
        }
859
    }
860
102
    None
861
}
862

            
863
285
fn get_cookies_section(request: &Request) -> Option<&Section> {
864
285
    for s in &request.sections {
865
69
        if let SectionValue::Cookies(_) = s.value {
866
9
            return Some(s);
867
        }
868
    }
869
276
    None
870
}
871

            
872
285
fn get_form_params_section(request: &Request) -> Option<&Section> {
873
285
    for s in &request.sections {
874
60
        if let SectionValue::FormParams(_, _) = s.value {
875
6
            return Some(s);
876
        }
877
    }
878
279
    None
879
}
880

            
881
285
fn get_option_section(request: &Request) -> Option<&Section> {
882
285
    for s in &request.sections {
883
78
        if let SectionValue::Options(_) = s.value {
884
42
            return Some(s);
885
        }
886
    }
887
243
    None
888
}
889

            
890
285
fn get_multipart_section(request: &Request) -> Option<&Section> {
891
285
    for s in &request.sections {
892
66
        if let SectionValue::MultipartFormData(_, _) = s.value {
893
6
            return Some(s);
894
        }
895
    }
896
279
    None
897
}
898

            
899
285
fn get_query_params_section(request: &Request) -> Option<&Section> {
900
285
    for s in &request.sections {
901
51
        if let SectionValue::QueryParams(_, _) = s.value {
902
12
            return Some(s);
903
        }
904
    }
905
273
    None
906
}
907

            
908
285
fn get_basic_auth_section(request: &Request) -> Option<&Section> {
909
285
    for s in &request.sections {
910
3
        if let SectionValue::BasicAuth(Some(_)) = s.value {
911
3
            return Some(s);
912
        }
913
    }
914
282
    None
915
}
916

            
917
42
fn lint_duration_option(option: &DurationOption, default_unit: DurationUnit) -> String {
918
42
    match option {
919
30
        DurationOption::Literal(duration) => lint_duration(duration, default_unit),
920
12
        DurationOption::Placeholder(expr) => expr.lint(),
921
    }
922
}
923

            
924
30
fn lint_duration(duration: &Duration, default_unit: DurationUnit) -> String {
925
30
    let mut s = String::new();
926
30
    s.push_str(&duration.value.lint());
927
30
    let unit = duration.unit.unwrap_or(default_unit);
928
30
    s.push_str(&unit.to_string());
929
30
    s
930
}
931

            
932
#[cfg(test)]
933
mod tests {
934
    use crate::linter::lint_hurl_file;
935
    use hurl_core::parser;
936

            
937
    #[test]
938
    fn test_lint_hurl_file() {
939
        let src = r#"
940
    # comment 1
941
  #comment 2 with trailing spaces    
942
  GET   https://foo.com
943
[Form]
944
  bar : baz
945
  [Options]
946
 location : true
947
HTTP   200"#;
948
        let file = parser::parse_hurl_file(src).unwrap();
949
        let linted = lint_hurl_file(&file);
950
        assert_eq!(
951
            linted,
952
            r#"
953
# comment 1
954
#comment 2 with trailing spaces
955
GET https://foo.com
956
[Options]
957
location: true
958
[Form]
959
bar: baz
960
HTTP 200
961
"#
962
        );
963
    }
964
}