diff_man/
parser.rs

1use {
2    crate::diff::*,
3    std::{path::PathBuf, str::FromStr},
4};
5
6pub struct Parser {}
7#[derive(Debug)]
8enum ParserState {
9    Init,
10    Command,
11    Index,
12    OriginPath,
13    NewPath,
14    Hunk,
15    LineChange(Change),
16}
17
18#[derive(Debug)]
19pub struct ParseError {
20    kind: ParseErrorKind,
21    reason: String,
22    line: String,
23}
24#[derive(Debug)]
25pub enum ParseErrorKind {
26    InvalidLineStart,
27    ExpectationFailed,
28    InvalidLine,
29}
30
31impl Parser {
32    fn parse_line_kind(state: &ParserState, line: &str) -> Line {
33        match state {
34            ParserState::Init => {
35                if line.starts_with("diff") {
36                    Line::Command
37                } else {
38                    Line::Unknown
39                }
40            }
41            ParserState::Command => {
42                if line.starts_with("index") {
43                    Line::Index
44                } else if line.starts_with(DIFF_SIGN_HEADER_ORIGIN) {
45                    Line::OrignPath
46                } else {
47                    Line::Unknown
48                }
49            }
50            ParserState::Index => {
51                if line.starts_with(DIFF_SIGN_HEADER_ORIGIN) {
52                    Line::OrignPath
53                } else {
54                    Line::Unknown
55                }
56            }
57            ParserState::OriginPath => {
58                if line.starts_with(DIFF_SIGN_HEADER_NEW) {
59                    Line::NewPath
60                } else {
61                    Line::Unknown
62                }
63            }
64            ParserState::NewPath => {
65                if line.starts_with(DIFF_SIGN_HUNK) {
66                    Line::Hunk
67                } else {
68                    Line::Unknown
69                }
70            }
71            ParserState::Hunk => match line.split_at(1) {
72                (DIFF_SIGN_LINE_ADDED, _) => Line::LineChange(Change::Added),
73                (DIFF_SIGN_LINE_DEFAULT, _) => {
74                    Line::LineChange(Change::Default)
75                }
76                (DIFF_SIGN_LINE_DELETED, _) => {
77                    Line::LineChange(Change::Deleted)
78                }
79                _ => Line::Unknown,
80            },
81            ParserState::LineChange(_) => {
82                if line.starts_with("diff") {
83                    Line::Command
84                } else if line.starts_with("index") {
85                    Line::Index
86                } else if line.starts_with(DIFF_SIGN_HEADER_ORIGIN) {
87                    Line::OrignPath
88                } else if line.starts_with(DIFF_SIGN_HUNK) {
89                    Line::Hunk
90                } else {
91                    match line.split_at(1) {
92                        (DIFF_SIGN_LINE_ADDED, _) => {
93                            Line::LineChange(Change::Added)
94                        }
95                        (DIFF_SIGN_LINE_DEFAULT, _) => {
96                            Line::LineChange(Change::Default)
97                        }
98                        (DIFF_SIGN_LINE_DELETED, _) => {
99                            Line::LineChange(Change::Deleted)
100                        }
101                        _ => Line::Unknown,
102                    }
103                }
104            }
105        }
106    }
107
108    fn parse_line_content<'line>(
109        line: &'line str,
110        kind: &Line,
111    ) -> Result<&'line str, ParseError> {
112        let content = match kind {
113            Line::Command => line,
114            Line::Index => {
115                line.strip_prefix("index ").ok_or_else(|| ParseError {
116                    kind: ParseErrorKind::ExpectationFailed,
117                    reason: "expect line start with `index `".to_string(),
118                    line: line.to_string(),
119                })?
120            }
121            Line::OrignPath => {
122                line.strip_prefix("--- ").ok_or_else(|| ParseError {
123                    kind: ParseErrorKind::ExpectationFailed,
124                    reason: "expect line start with `--- `".to_string(),
125                    line: line.to_string(),
126                })?
127            }
128            Line::NewPath => {
129                line.strip_prefix("+++ ").ok_or_else(|| ParseError {
130                    kind: ParseErrorKind::ExpectationFailed,
131                    reason: "expect line start with `+++ `".to_string(),
132                    line: line.to_string(),
133                })?
134            }
135            Line::Hunk => {
136                let end_offset =
137                    line.find(" @@").ok_or_else(|| ParseError {
138                        kind: ParseErrorKind::ExpectationFailed,
139                        reason: "cannot find hunk end with ` @@`".to_string(),
140                        line: line.to_string(),
141                    })?;
142                line.split_at(end_offset).0.strip_prefix("@@ ").ok_or_else(
143                    || ParseError {
144                        kind: ParseErrorKind::ExpectationFailed,
145                        reason: "expect line start with `@@ `".to_string(),
146                        line: line.to_string(),
147                    },
148                )?
149            }
150            Line::LineChange(_) => line.split_at(1).1,
151            // this should be unreachable
152            Line::Unknown => panic!("unknown line start"),
153        };
154
155        Ok(content)
156    }
157
158    pub fn parse_git_udiff(src: &str) -> Result<DiffComposition, ParseError> {
159        let mut state = ParserState::Init;
160        // State
161        //  command     diff --git a/tests/vm.rs b/tests/vm.rs
162        //  index       index 90d5af1..30044cb 100644
163        //  old_path    --- a/tests/vm.rs
164        //  new_path    +++ b/tests/vm.rs
165        //  hunk        @@ -16,7 +16,9 @@ scope..
166        //      linechange... |+|
167        //      linechange... |-|
168        //      linechange... | |
169        //      *hunk        @@ ...
170        let mut diffcom = DiffComposition {
171            format: DiffFormat::GitUdiff,
172            diff: Vec::new(),
173        };
174
175        let mut diff_cur: Option<Diff> = None;
176        let mut hunk_cur: Option<DiffHunk> = None;
177
178        for line in src.lines() {
179            let tag = Self::parse_line_kind(&state, line);
180            state = match &tag {
181                Line::Command => ParserState::Command,
182                Line::Index => ParserState::Index,
183                Line::OrignPath => ParserState::OriginPath,
184                Line::NewPath => ParserState::NewPath,
185                Line::Hunk => ParserState::Hunk,
186                Line::LineChange(change_kind) => {
187                    ParserState::LineChange(*change_kind)
188                }
189                Line::Unknown => Err(ParseError {
190                    kind: ParseErrorKind::InvalidLineStart,
191                    reason: "line starting with invalid token".to_string(),
192                    line: line.to_string(),
193                })?,
194            };
195            let content = Self::parse_line_content(line, &tag)?;
196            match state {
197                ParserState::Init => unreachable!(),
198                ParserState::Command => {
199                    if diff_cur.is_some() {
200                        let mut diff_before = diff_cur.take().unwrap();
201
202                        if hunk_cur.is_some() {
203                            let hunk_before = hunk_cur.take().unwrap();
204                            diff_before.hunk.push(hunk_before);
205                        }
206                        diffcom.diff.push(diff_before);
207                    }
208                    let (file_path_a, file_path_b) = content
209                        .strip_prefix("diff --git ")
210                        .and_then(|s|s.split_once(' '))
211                        .ok_or_else(||{
212                            ParseError{
213                                kind:ParseErrorKind::ExpectationFailed,
214                                reason:"lines not starting with `diff --git ` or cannot split command's arguments".to_string(
215                                ),
216                                line:line.to_string(),
217                            }
218                        })?;
219                    let file_path_a = file_path_a
220                        .strip_prefix("a/")
221                        .ok_or_else(|| ParseError {
222                            kind: ParseErrorKind::ExpectationFailed,
223                            reason: "expect to path_a start with `a/`"
224                                .to_string(),
225                            line: line.to_string(),
226                        })?;
227
228                    let file_path_b = file_path_b
229                        .strip_prefix("b/")
230                        .ok_or_else(|| ParseError {
231                            kind: ParseErrorKind::ExpectationFailed,
232                            reason: "expect to path_b start with `b/`"
233                                .to_string(),
234                            line: line.to_string(),
235                        })?;
236                    if file_path_a != file_path_b {
237                        Err(ParseError {
238                            kind: ParseErrorKind::ExpectationFailed,
239                            reason: "file path a and b are different"
240                                .to_string(),
241                            line: line.to_string(),
242                        })?;
243                    }
244                    let path = PathBuf::from_str(file_path_a).map_err(|e| {
245                        ParseError {
246                            kind: ParseErrorKind::ExpectationFailed,
247                            reason: format!("cannot parse file_path, {:?}", e),
248                            line: line.to_string(),
249                        }
250                    })?;
251
252                    diff_cur = Some(Diff {
253                        path,
254                        hunk: Vec::new(),
255                        command: Some(content.to_string()),
256                        index: None,
257                    });
258                }
259                ParserState::Index => match &mut diff_cur {
260                    Some(cur) => {
261                        if cur.index.is_some() {
262                            Err(ParseError {
263                                kind: ParseErrorKind::InvalidLine,
264                                reason: format!(
265                                    "there is index in current diff {:?}",
266                                    &diff_cur
267                                ),
268                                line: line.to_string(),
269                            })?;
270                        } else {
271                            cur.index = Some(content.to_string())
272                        }
273                    }
274                    None => {
275                        Err(ParseError {
276                            kind: ParseErrorKind::ExpectationFailed,
277                            reason: format!(
278                                "there is no current diff {:?}",
279                                &diff_cur
280                            ),
281                            line: line.to_string(),
282                        })?;
283                    }
284                },
285                ParserState::OriginPath => match &diff_cur {
286                    Some(d) => {
287                        let diff_path =
288                            d.path.to_str().ok_or_else(|| ParseError {
289                                kind: ParseErrorKind::ExpectationFailed,
290                                reason: "cannot convert diff path to str"
291                                    .to_string(),
292                                line: line.to_string(),
293                            })?;
294
295                        let origin_path = content
296                            .strip_prefix("a/")
297                            .ok_or_else(|| ParseError {
298                                kind: ParseErrorKind::ExpectationFailed,
299                                reason: "old file path not start with `a/`"
300                                    .to_string(),
301                                line: line.to_string(),
302                            })?;
303                        if diff_path != origin_path {
304                            Err(ParseError {
305                                kind: ParseErrorKind::ExpectationFailed,
306                                reason: format!(
307                                    "diff path and origin path is different, [diff: {}] [origin: {}]",
308                                    diff_path, origin_path
309                                ),
310                                line: line.to_string(),
311                            })?;
312                        }
313                    }
314                    None => {
315                        Err(ParseError {
316                            kind: ParseErrorKind::ExpectationFailed,
317                            reason: format!(
318                                "there is no current diff {:?}",
319                                &diff_cur
320                            ),
321                            line: line.to_string(),
322                        })?;
323                    }
324                },
325                ParserState::NewPath => match &diff_cur {
326                    Some(d) => {
327                        let diff_path =
328                            d.path.to_str().ok_or_else(|| ParseError {
329                                kind: ParseErrorKind::ExpectationFailed,
330                                reason: "cannot convert diff path to str"
331                                    .to_string(),
332                                line: line.to_string(),
333                            })?;
334
335                        let new_path =
336                            content.strip_prefix("b/").ok_or_else(|| {
337                                ParseError {
338                                    kind: ParseErrorKind::ExpectationFailed,
339                                    reason: "old file path not start with `b/`"
340                                        .to_string(),
341                                    line: line.to_string(),
342                                }
343                            })?;
344                        if diff_path != new_path {
345                            Err(ParseError {
346                                kind: ParseErrorKind::ExpectationFailed,
347                                reason: format!(
348                                    "diff path and new path is different, [diff: {}] [new: {}]",
349                                    diff_path, new_path
350                                ),
351                                line: line.to_string(),
352                            })?;
353                        }
354                    }
355                    None => {
356                        Err(ParseError {
357                            kind: ParseErrorKind::ExpectationFailed,
358                            reason: format!(
359                                "there is no current diff {:?}",
360                                &diff_cur
361                            ),
362                            line: line.to_string(),
363                        })?;
364                    }
365                },
366                ParserState::Hunk => match &mut diff_cur {
367                    Some(dc) => {
368                        if let Some(hunk_before) = hunk_cur.take() {
369                            dc.hunk.push(hunk_before)
370                        }
371                        let (old, new) =
372                            content.split_once(' ').ok_or_else(|| {
373                                ParseError {
374                                    kind: ParseErrorKind::ExpectationFailed,
375                                    reason: "there is no space in hunk line"
376                                        .to_string(),
377                                    line: line.to_string(),
378                                }
379                            })?;
380                        let (old_line, old_len) =
381                            old.split_once(',').ok_or_else(|| ParseError {
382                                kind: ParseErrorKind::ExpectationFailed,
383                                reason: "cannot split hunk old range with `,`"
384                                    .to_string(),
385                                line: line.to_string(),
386                            })?;
387                        let (new_line, new_len) =
388                            new.split_once(',').ok_or_else(|| ParseError {
389                                kind: ParseErrorKind::ExpectationFailed,
390                                reason: "cannot split hunk new range with `,`"
391                                    .to_string(),
392                                line: line.to_string(),
393                            })?;
394
395                        let old_line = old_line
396                            .strip_prefix('-')
397                            .ok_or_else(|| ParseError {
398                                kind: ParseErrorKind::ExpectationFailed,
399                                reason: "cannot strip `-` of old_line"
400                                    .to_string(),
401                                line: line.to_string(),
402                            })?
403                            .parse::<usize>()
404                            .map_err(|e| ParseError {
405                                kind: ParseErrorKind::ExpectationFailed,
406                                reason: format!(
407                                    "cannot parse old_line to usize, {:?}",
408                                    e
409                                ),
410                                line: line.to_string(),
411                            })?;
412                        let old_len =
413                            old_len.parse::<usize>().map_err(|e| {
414                                ParseError {
415                                    kind: ParseErrorKind::ExpectationFailed,
416                                    reason: format!(
417                                        "cannot parse old_len to usize, {:?}",
418                                        e
419                                    ),
420                                    line: line.to_string(),
421                                }
422                            })?;
423                        let new_line = new_line
424                            .strip_prefix('+')
425                            .ok_or_else(|| ParseError {
426                                kind: ParseErrorKind::ExpectationFailed,
427                                reason: "cannot strip `+` of new_line"
428                                    .to_string(),
429                                line: line.to_string(),
430                            })?
431                            .parse::<usize>()
432                            .map_err(|e| ParseError {
433                                kind: ParseErrorKind::ExpectationFailed,
434                                reason: format!(
435                                    "cannot parse new_line to usize, {:?}",
436                                    e
437                                ),
438                                line: line.to_string(),
439                            })?;
440                        let new_len =
441                            new_len.parse::<usize>().map_err(|e| {
442                                ParseError {
443                                    kind: ParseErrorKind::ExpectationFailed,
444                                    reason: format!(
445                                        "cannot parse new_len to usize, {:?}",
446                                        e
447                                    ),
448                                    line: line.to_string(),
449                                }
450                            })?;
451
452                        hunk_cur = Some(DiffHunk {
453                            old_line,
454                            old_len,
455                            new_line,
456                            new_len,
457                            change: Vec::new(),
458                        });
459                    }
460                    None => {
461                        panic!("there is no current diff {:?}", &diff_cur)
462                    }
463                },
464                ParserState::LineChange(kind) => match &mut hunk_cur {
465                    Some(h) => {
466                        let change = LineChange {
467                            kind,
468                            content: content.to_string(),
469                        };
470                        h.change.push(change)
471                    }
472                    None => {
473                        Err(ParseError {
474                            kind: ParseErrorKind::ExpectationFailed,
475                            reason: format!(
476                                "there is no current hunk. current diff {:?}",
477                                &diff_cur
478                            ),
479                            line: line.to_string(),
480                        })?;
481                    }
482                },
483            }
484        }
485
486        if let Some(hunk) = hunk_cur {
487            match &mut diff_cur {
488                Some(c) => c.hunk.push(hunk),
489                None => {
490                    Err(ParseError {
491                        kind: ParseErrorKind::ExpectationFailed,
492                        reason: "there is no diff_cur to add hunk".to_string(),
493                        line: "".to_string(),
494                    })?;
495                }
496            }
497        }
498        if let Some(diff) = diff_cur {
499            diffcom.diff.push(diff);
500        } else {
501            Err(ParseError {
502                kind: ParseErrorKind::ExpectationFailed,
503                reason: "there is no diff_cur at end".to_string(),
504                line: "".to_string(),
505            })?;
506        }
507
508        Ok(diffcom)
509    }
510}
511
512#[cfg(test)]
513mod test {
514    use core::panic;
515
516    use crate::{
517        diff::*,
518        parser::{Parser, ParserState},
519    };
520
521    const short_test_data: &str = r#"diff --git a/tests/vm.rs b/tests/vm.rs
522index 90d5af1..30044cb 100644
523--- a/tests/vm.rs
524+++ b/tests/vm.rs
525@@ -16,7 +16,9 @@ fn run_vm_test(tests: Tests<Option<Object>>) {
526         let program = Parser::new(lexer).parse().unwrap();
527 
528         let mut comp = Compiler::create().unwrap();
529-        comp.compile(program);
530+        if let Err(e) = comp.compile(program) {
531+            panic!("Compile error {:?}", e);
532+        }
533         let bytecode = comp.bytecode().unwrap();
534 
535         println!("Bytecode\n{}", bytecode.to_string());
536@@ -25,7 +27,7 @@ fn run_vm_test(tests: Tests<Option<Object>>) {
537 
538         while vm.is_runable() {
539             if let Err(err) = vm.run_single() {
540-                eprintln!("Error {:?}", err);
541+                panic!("VmError {:?}", err)
542             }
543         }
544         println!("VM STACK:\n {}", vm.stack_to_string());
545@@ -262,8 +264,7 @@ let no_return = fn() { };no_return() no_return() no_return() no_return()
546 
547     tests.add((
548         "
549-let fun = fn() { 10 + 20 };
550-fun()
551+let fun = fn() { 10 + 20 }; fun()
552 ",
553         Some(Object::Int(Int { value: 30 })),
554     ));
555"#;
556    #[test]
557    fn test_parse_linestart() {
558        let mut state = ParserState::Init;
559        for line in short_test_data.lines() {
560            let tag = Parser::parse_line_kind(&state, line);
561            state = match &tag {
562                Line::Command => ParserState::Command,
563                Line::Index => ParserState::Index,
564                Line::OrignPath => ParserState::OriginPath,
565                Line::NewPath => ParserState::NewPath,
566                Line::Hunk => ParserState::Hunk,
567                Line::LineChange(change_kind) => {
568                    ParserState::LineChange(*change_kind)
569                }
570                Line::Unknown => panic!("Unknown line start"),
571            };
572
573            println!("[S:{:?}] [T:{:?}] -- L>{}", &state, &tag, &line);
574        }
575    }
576
577    #[test]
578    fn test_parse_udiff() {
579        let com = Parser::parse_git_udiff(short_test_data).unwrap();
580        println!("{:#?}", com);
581    }
582}