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, DurationOption, Entry, EntryOption, File, FilenameParam,
21
    FilenameValue, FilterValue, Hex, HurlFile, IntegerValue, JsonValue, KeyValue, LineTerminator,
22
    Method, MultilineString, MultipartParam, NaturalOption, Number, OptionKind, Placeholder,
23
    Predicate, PredicateFuncValue, PredicateValue, Query, QueryValue, Regex, RegexValue, Request,
24
    Response, Section, SectionValue, StatusValue, Template, VariableDefinition, VariableValue,
25
    VerbosityOption, VersionValue, I64, U64,
26
};
27
use hurl_core::types::{Count, Duration, 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
381
    fn lint(&self) -> String {
41
381
        let mut s = String::new();
42
381
        self.line_terminators
43
381
            .iter()
44
381
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
45
381
        s.push_str(&self.query.lint());
46
381
        if !self.filters.is_empty() {
47
114
            s.push(' ');
48
114
            let filters = self
49
114
                .filters
50
114
                .iter()
51
179
                .map(|(_, f)| f.value.lint())
52
114
                .collect::<Vec<_>>()
53
114
                .join(" ");
54
114
            s.push_str(&filters);
55
        }
56
381
        s.push(' ');
57
381
        s.push_str(&self.predicate.lint());
58
381
        s.push_str(&lint_lt(&self.line_terminator0, true));
59
381
        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
99
    fn lint(&self) -> String {
75
99
        let mut s = String::new();
76
99
        self.line_terminators
77
99
            .iter()
78
104
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
79
99
        s.push_str(&self.value.lint());
80
99
        s.push_str(&lint_lt(&self.line_terminator0, true));
81
99
        s
82
    }
83
}
84

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

            
94
impl Lint for Bytes {
95
99
    fn lint(&self) -> String {
96
99
        match self {
97
6
            Bytes::Json(value) => value.lint(),
98
3
            Bytes::Xml(value) => value.clone(),
99
42
            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
312
    fn lint(&self) -> String {
166
312
        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
282
    fn lint(&self) -> String {
187
282
        let mut s = String::new();
188
282
        s.push_str(&self.request.lint());
189
282
        if let Some(response) = &self.response {
190
114
            s.push_str(&response.lint());
191
        }
192
282
        s
193
    }
194
}
195

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

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

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

            
356
impl Lint for IntegerValue {
357
3
    fn lint(&self) -> String {
358
3
        match self {
359
3
            IntegerValue::Literal(value) => value.lint(),
360
            IntegerValue::Placeholder(value) => value.lint(),
361
        }
362
    }
363
}
364

            
365
impl Lint for I64 {
366
3
    fn lint(&self) -> String {
367
3
        self.to_source().to_string()
368
    }
369
}
370

            
371
impl Lint for JsonValue {
372
6
    fn lint(&self) -> String {
373
6
        self.to_source().to_string()
374
    }
375
}
376

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

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

            
413
impl Lint for Method {
414
282
    fn lint(&self) -> String {
415
282
        self.to_source().to_string()
416
    }
417
}
418

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

            
430
impl Lint for MultilineString {
431
60
    fn lint(&self) -> String {
432
60
        self.to_source().to_string()
433
    }
434
}
435

            
436
impl Lint for NaturalOption {
437
6
    fn lint(&self) -> String {
438
6
        match self {
439
3
            NaturalOption::Literal(value) => value.lint(),
440
3
            NaturalOption::Placeholder(value) => value.lint(),
441
        }
442
    }
443
}
444

            
445
impl Lint for Number {
446
108
    fn lint(&self) -> String {
447
108
        self.to_source().to_string()
448
    }
449
}
450

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

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

            
559
impl Lint for Placeholder {
560
84
    fn lint(&self) -> String {
561
84
        self.to_source().to_string()
562
    }
563
}
564

            
565
impl Lint for Predicate {
566
381
    fn lint(&self) -> String {
567
381
        let mut s = String::new();
568
381
        if self.not {
569
3
            s.push_str("not");
570
3
            s.push(' ');
571
        }
572
381
        s.push_str(&self.predicate_func.value.lint());
573
381
        s
574
    }
575
}
576

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

            
646
impl Lint for PredicateValue {
647
321
    fn lint(&self) -> String {
648
321
        match self {
649
3
            PredicateValue::Base64(value) => value.lint(),
650
3
            PredicateValue::Bool(value) => value.to_string(),
651
3
            PredicateValue::File(value) => value.lint(),
652
27
            PredicateValue::Hex(value) => value.lint(),
653
18
            PredicateValue::MultilineString(value) => value.lint(),
654
3
            PredicateValue::Null => "null".to_string(),
655
108
            PredicateValue::Number(value) => value.lint(),
656
3
            PredicateValue::Placeholder(value) => value.lint(),
657
6
            PredicateValue::Regex(value) => value.lint(),
658
147
            PredicateValue::String(value) => value.lint(),
659
        }
660
    }
661
}
662

            
663
impl Lint for Regex {
664
12
    fn lint(&self) -> String {
665
12
        self.to_source().to_string()
666
    }
667
}
668

            
669
impl Lint for RegexValue {
670
9
    fn lint(&self) -> String {
671
9
        match self {
672
3
            RegexValue::Template(value) => value.lint(),
673
6
            RegexValue::Regex(value) => value.lint(),
674
        }
675
    }
676
}
677

            
678
impl Lint for Request {
679
282
    fn lint(&self) -> String {
680
282
        let mut s = String::new();
681
282
        self.line_terminators
682
282
            .iter()
683
346
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
684
282
        s.push_str(&self.method.lint());
685
282
        s.push(' ');
686
282
        s.push_str(&self.url.lint());
687
282
        s.push_str(&lint_lt(&self.line_terminator0, true));
688

            
689
307
        self.headers.iter().for_each(|h| s.push_str(&h.lint()));
690

            
691
        // We rewrite our file and reorder the various section.
692
282
        if let Some(section) = get_option_section(self) {
693
42
            s.push_str(&section.lint());
694
        }
695
282
        if let Some(section) = get_query_params_section(self) {
696
12
            s.push_str(&section.lint());
697
        }
698
282
        if let Some(section) = get_basic_auth_section(self) {
699
3
            s.push_str(&section.lint());
700
        }
701
282
        if let Some(section) = get_form_params_section(self) {
702
6
            s.push_str(&section.lint());
703
        }
704
282
        if let Some(section) = get_multipart_section(self) {
705
6
            s.push_str(&section.lint());
706
        }
707
282
        if let Some(section) = get_cookies_section(self) {
708
9
            s.push_str(&section.lint());
709
        }
710
282
        if let Some(body) = &self.body {
711
54
            s.push_str(&body.lint());
712
        }
713
282
        s
714
    }
715
}
716

            
717
impl Lint for Response {
718
114
    fn lint(&self) -> String {
719
114
        let mut s = String::new();
720
114
        self.line_terminators
721
114
            .iter()
722
116
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
723
114
        s.push_str(&self.version.value.lint());
724
114
        s.push(' ');
725
114
        s.push_str(&self.status.value.lint());
726
114
        s.push_str(&lint_lt(&self.line_terminator0, true));
727

            
728
119
        self.headers.iter().for_each(|h| s.push_str(&h.lint()));
729

            
730
114
        if let Some(section) = get_captures_section(self) {
731
12
            s.push_str(&section.lint());
732
        }
733
114
        if let Some(section) = get_asserts_section(self) {
734
48
            s.push_str(&section.lint());
735
        }
736
114
        if let Some(body) = &self.body {
737
45
            s.push_str(&body.lint());
738
        }
739
114
        s
740
    }
741
}
742

            
743
impl Lint for Section {
744
138
    fn lint(&self) -> String {
745
138
        let mut s = String::new();
746
138
        self.line_terminators
747
138
            .iter()
748
148
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
749
138
        s.push('[');
750
138
        s.push_str(self.identifier());
751
138
        s.push(']');
752
138
        s.push_str(&lint_lt(&self.line_terminator0, true));
753
138
        s.push_str(&self.value.lint());
754
138
        s
755
    }
756
}
757

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

            
792
impl Lint for StatusValue {
793
114
    fn lint(&self) -> String {
794
114
        self.to_source().to_string()
795
    }
796
}
797

            
798
impl Lint for Template {
799
1188
    fn lint(&self) -> String {
800
1188
        self.to_source().to_string()
801
    }
802
}
803

            
804
impl Lint for U64 {
805
33
    fn lint(&self) -> String {
806
33
        self.to_source().to_string()
807
    }
808
}
809

            
810
impl Lint for VariableDefinition {
811
27
    fn lint(&self) -> String {
812
27
        let mut s = String::new();
813
27
        s.push_str(&self.name);
814
27
        s.push('=');
815
27
        s.push_str(&self.value.lint());
816
27
        s
817
    }
818
}
819

            
820
impl Lint for VariableValue {
821
27
    fn lint(&self) -> String {
822
27
        self.to_source().to_string()
823
    }
824
}
825

            
826
impl Lint for VersionValue {
827
114
    fn lint(&self) -> String {
828
114
        self.to_source().to_string()
829
    }
830
}
831

            
832
impl Lint for VerbosityOption {
833
6
    fn lint(&self) -> String {
834
6
        self.to_string()
835
    }
836
}
837

            
838
114
fn get_asserts_section(response: &Response) -> Option<&Section> {
839
126
    for s in &response.sections {
840
60
        if let SectionValue::Asserts(_) = s.value {
841
48
            return Some(s);
842
        }
843
    }
844
66
    None
845
}
846

            
847
114
fn get_captures_section(response: &Response) -> Option<&Section> {
848
153
    for s in &response.sections {
849
51
        if let SectionValue::Captures(_) = s.value {
850
12
            return Some(s);
851
        }
852
    }
853
102
    None
854
}
855

            
856
282
fn get_cookies_section(request: &Request) -> Option<&Section> {
857
342
    for s in &request.sections {
858
69
        if let SectionValue::Cookies(_) = s.value {
859
9
            return Some(s);
860
        }
861
    }
862
273
    None
863
}
864

            
865
282
fn get_form_params_section(request: &Request) -> Option<&Section> {
866
336
    for s in &request.sections {
867
60
        if let SectionValue::FormParams(_, _) = s.value {
868
6
            return Some(s);
869
        }
870
    }
871
276
    None
872
}
873

            
874
282
fn get_option_section(request: &Request) -> Option<&Section> {
875
318
    for s in &request.sections {
876
78
        if let SectionValue::Options(_) = s.value {
877
42
            return Some(s);
878
        }
879
    }
880
240
    None
881
}
882

            
883
282
fn get_multipart_section(request: &Request) -> Option<&Section> {
884
342
    for s in &request.sections {
885
66
        if let SectionValue::MultipartFormData(_, _) = s.value {
886
6
            return Some(s);
887
        }
888
    }
889
276
    None
890
}
891

            
892
282
fn get_query_params_section(request: &Request) -> Option<&Section> {
893
321
    for s in &request.sections {
894
51
        if let SectionValue::QueryParams(_, _) = s.value {
895
12
            return Some(s);
896
        }
897
    }
898
270
    None
899
}
900

            
901
282
fn get_basic_auth_section(request: &Request) -> Option<&Section> {
902
345
    for s in &request.sections {
903
3
        if let SectionValue::BasicAuth(Some(_)) = s.value {
904
3
            return Some(s);
905
        }
906
    }
907
279
    None
908
}
909

            
910
42
fn lint_duration_option(option: &DurationOption, default_unit: DurationUnit) -> String {
911
42
    match option {
912
30
        DurationOption::Literal(duration) => lint_duration(duration, default_unit),
913
12
        DurationOption::Placeholder(expr) => expr.lint(),
914
    }
915
}
916

            
917
30
fn lint_duration(duration: &Duration, default_unit: DurationUnit) -> String {
918
30
    let mut s = String::new();
919
30
    s.push_str(&duration.value.lint());
920
30
    let unit = duration.unit.unwrap_or(default_unit);
921
30
    s.push_str(&unit.to_string());
922
30
    s
923
}
924

            
925
#[cfg(test)]
926
mod tests {
927
    use crate::linter::lint_hurl_file;
928
    use hurl_core::parser;
929

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