Repository: reviewdog/reviewdog
Pull request #699 (2020-07-26 08:18:39)

Support -f=diff especially for suggested fix
- [x] Updated Unreleased section in [CHANGELOG](https://github.com/reviewdog/reviewdog/blob/master/CHANGELOG.md) or it's not notable changes.


Patch:
diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml
index de63e70f..2adbf4ec 100644
--- a/.github/workflows/reviewdog.yml
+++ b/.github/workflows/reviewdog.yml
@@ -161,6 +161,13 @@ jobs:
           cat ./_testdata/custom_rdjsonl.json | \
             reviewdog -name="custom-rdjsonl" -f=rdjsonl -reporter=github-pr-review
 
+      - name: gofmt -s with reviewdog
+        env:
+          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          gofmt -s -d . | \
+            reviewdog -name="gofmt" -f=diff -f.diff.strip=0 -reporter=github-pr-review
+
   golangci-lint:
     if: github.event_name == 'pull_request'
     name: runner / golangci-lint
@@ -170,7 +177,7 @@ jobs:
       - uses: reviewdog/action-golangci-lint@v1
         with:
           github_token: ${{ secrets.github_token }}
-          golangci_lint_flags: "--enable-all --exclude-use-default=false"
+          golangci_lint_flags: "--enable-all --exclude-use-default=false -D wsl -D testpackage"
           level: "warning"
           reporter: github-pr-check
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac97d844..4dfdecd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
  - [#674](https://github.com/reviewdog/reviewdog/pull/674) Support rdjsonl as input format
  - [#680](https://github.com/reviewdog/reviewdog/pull/680) github-pr-review: Support multiline comments
  - [#675](https://github.com/reviewdog/reviewdog/pull/675) [#698](https://github.com/reviewdog/reviewdog/pull/698) github-pr-review: Support suggested changes
+ - [#699](https://github.com/reviewdog/reviewdog/pull/699) Support diff input format (`-f=diff`). Useful for suggested changes.
 - ...
 
 ### :bug: Fixes
diff --git a/cmd/reviewdog/main.go b/cmd/reviewdog/main.go
index 47e03100..948af24e 100644
--- a/cmd/reviewdog/main.go
+++ b/cmd/reviewdog/main.go
@@ -47,7 +47,8 @@ type option struct {
 	diffCmd          string
 	diffStrip        int
 	efms             strslice
-	f                string // errorformat name
+	f                string // format name
+	fDiffStrip       int
 	list             bool   // list supported errorformat name
 	name             string // tool name which is used in comment
 	ci               string
@@ -67,6 +68,7 @@ const (
 	diffStripDoc        = "strip NUM leading components from diff file names (equivalent to 'patch -p') (default is 1 for git diff)"
 	efmsDoc             = `list of errorformat (https://github.com/reviewdog/errorformat)`
 	fDoc                = `format name (run -list to see supported format name) for input. It's also used as tool name in review comment if -name is empty`
+	fDiffStripDoc       = `option for -f=diff: strip NUM leading components from diff file names (equivalent to 'patch -p') (default is 1 for git diff)`
 	listDoc             = `list supported pre-defined format names which can be used as -f arg`
 	nameDoc             = `tool name in review comment. -f is used as tool name if -name is empty`
 	ciDoc               = `[deprecated] reviewdog automatically get necessary data. See also -reporter for migration`
@@ -177,6 +179,7 @@ func init() {
 	flag.IntVar(&opt.diffStrip, "strip", 1, diffStripDoc)
 	flag.Var(&opt.efms, "efm", efmsDoc)
 	flag.StringVar(&opt.f, "f", "", fDoc)
+	flag.IntVar(&opt.fDiffStrip, "f.diff.strip", 1, fDiffStripDoc)
 	flag.BoolVar(&opt.list, "list", false, listDoc)
 	flag.StringVar(&opt.name, "name", "", nameDoc)
 	flag.StringVar(&opt.ci, "ci", "", ciDoc)
@@ -670,7 +673,11 @@ func readConf(conf string) ([]byte, error) {
 }
 
 func newParserFromOpt(opt *option) (parser.Parser, error) {
-	p, err := parser.New(&parser.Option{FormatName: opt.f, Errorformat: opt.efms})
+	p, err := parser.New(&parser.Option{
+		FormatName:  opt.f,
+		DiffStrip:   opt.fDiffStrip,
+		Errorformat: opt.efms,
+	})
 	if err != nil {
 		return nil, fmt.Errorf("fail to create parser. use either -f or -efm: %w", err)
 	}
diff --git a/diff/parse.go b/diff/parse.go
index 4dd643b5..7df438f9 100644
--- a/diff/parse.go
+++ b/diff/parse.go
@@ -11,14 +11,14 @@ import (
 )
 
 const (
-	tokenDiffGit        = "diff --git" // diff --git a/sample.old.txt b/sample.new.txt
-	tokenOldFile        = "---"        // --- sample.old.txt	2016-10-13 05:09:35.820791185 +0900
-	tokenNewFile        = "+++"        // +++ sample.new.txt	2016-10-13 05:15:26.839245048 +0900
-	tokenStartHunk      = "@@"         // @@ -1,3 +1,4 @@
-	tokenUnchangedLine  = " "          //  unchanged, contextual line
-	tokenAddedLine      = "+"          // +added line
-	tokenDeletedLine    = "-"          // -deleted line
-	tokenNoNewlineAtEOF = `\`          // \ No newline at end of file
+	tokenDiff           = "diff" // diff --git a/sample.old.txt b/sample.new.txt
+	tokenOldFile        = "---"  // --- sample.old.txt	2016-10-13 05:09:35.820791185 +0900
+	tokenNewFile        = "+++"  // +++ sample.new.txt	2016-10-13 05:15:26.839245048 +0900
+	tokenStartHunk      = "@@"   // @@ -1,3 +1,4 @@
+	tokenUnchangedLine  = " "    //  unchanged, contextual line
+	tokenAddedLine      = "+"    // +added line
+	tokenDeletedLine    = "-"    // -deleted line
+	tokenNoNewlineAtEOF = `\`    // \ No newline at end of file
 )
 
 var (
@@ -103,11 +103,11 @@ func (p *fileParser) parseHunks() ([]*Hunk, error) {
 		return nil, ErrNoHunks
 	}
 	if !bytes.HasPrefix(b, []byte(tokenStartHunk)) {
-		b, err := p.r.Peek(len(tokenDiffGit))
+		b, err := p.r.Peek(len(tokenDiff))
 		if err != nil {
 			return nil, ErrNoHunks
 		}
-		if bytes.HasPrefix(b, []byte(tokenDiffGit)) {
+		if bytes.HasPrefix(b, []byte(tokenDiff)) {
 			// git diff may contain a file diff with empty hunks.
 			// e.g. delete an empty file.
 			return []*Hunk{}, nil
@@ -211,17 +211,17 @@ LOOP:
 
 func parseExtendedHeader(r *bufio.Reader) []string {
 	var es []string
-	b, err := r.Peek(len(tokenDiffGit))
+	b, err := r.Peek(len(tokenDiff))
 	if err != nil {
 		return nil
 	}
-	// if starts with 'diff --git', parse extended header
-	if bytes.HasPrefix(b, []byte(tokenDiffGit)) {
+	// if starts with 'diff', parse extended header
+	if bytes.HasPrefix(b, []byte(tokenDiff)) {
 		diffgitline, _ := readline(r) // ignore err because we know it can read something
 		es = append(es, diffgitline)
 		for {
-			b, err := r.Peek(len(tokenDiffGit))
-			if err != nil || bytes.HasPrefix(b, []byte(tokenOldFile)) || bytes.HasPrefix(b, []byte(tokenDiffGit)) {
+			b, err := r.Peek(len(tokenDiff))
+			if err != nil || bytes.HasPrefix(b, []byte(tokenOldFile)) || bytes.HasPrefix(b, []byte(tokenDiff)) {
 				break
 			}
 			line, _ := readline(r)
diff --git a/filter/diff_filter.go b/filter/diff_filter.go
index 43af4fbe..43fca7a4 100644
--- a/filter/diff_filter.go
+++ b/filter/diff_filter.go
@@ -186,7 +186,7 @@ func NormalizeDiffPath(diffpath string, strip int) string {
 		return ""
 	}
 	path := diffpath
-	if strip > 0 {
+	if strip > 0 && !filepath.IsAbs(path) {
 		ps := splitPathList(path)
 		if len(ps) > strip {
 			path = filepath.Join(ps[strip:]...)
diff --git a/parser/diff.go b/parser/diff.go
new file mode 100644
index 00000000..4abf2ee2
--- /dev/null
+++ b/parser/diff.go
@@ -0,0 +1,132 @@
+package parser
+
+import (
+	"fmt"
+	"io"
+	"strings"
+
+	"github.com/reviewdog/reviewdog/diff"
+	"github.com/reviewdog/reviewdog/filter"
+	"github.com/reviewdog/reviewdog/proto/rdf"
+)
+
+var _ Parser = &DiffParser{}
+
+// DiffParser is a unified diff parser.
+type DiffParser struct {
+	strip int
+}
+
+// NewDiffParser creates a new DiffParser.
+func NewDiffParser(strip int) *DiffParser {
+	return &DiffParser{strip: strip}
+}
+
+// state data for a diagnostic.
+type dstate struct {
+	startLine     int
+	isInsert      bool
+	newLines      []string
+	originalLines []string // For Diagnostic.original_output
+}
+
+func (d dstate) build(path string, currentLine int) *rdf.Diagnostic {
+	drange := &rdf.Range{ // Diagnostic Range
+		Start: &rdf.Position{Line: int32(d.startLine)},
+		End:   &rdf.Position{Line: int32(currentLine)},
+	}
+	text := strings.Join(d.newLines, "\n")
+	if d.isInsert {
+		text += "\n" // Need line-break at the end if it's insertion,
+		drange.GetEnd().Line = int32(d.startLine)
+		drange.GetEnd().Column = 1
+		drange.GetStart().Column = 1
+	}
+	return &rdf.Diagnostic{
+		Location:       &rdf.Location{Path: path, Range: drange},
+		Suggestions:    []*rdf.Suggestion{{Range: drange, Text: text}},
+		OriginalOutput: strings.Join(d.originalLines, "\n"),
+	}
+}
+
+// Parse parses input as unified diff format and return it as diagnostics.
+func (p *DiffParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
+	filediffs, err := diff.ParseMultiFile(r)
+	if err != nil {
+		return nil, fmt.Errorf("fail to parse diff: %w", err)
+	}
+	diagnostics := []*rdf.Diagnostic{}
+	for _, fdiff := range filediffs {
+		path := filter.NormalizeDiffPath(fdiff.PathNew, p.strip)
+		for _, hunk := range fdiff.Hunks {
+			lnum := hunk.StartLineOld - 1
+			prevState := diff.LineUnchanged
+			state := dstate{}
+			emit := func() {
+				diagnostics = append(diagnostics, state.build(path, lnum))
+				state = dstate{}
+			}
+			for i, diffLine := range hunk.Lines {
+				switch diffLine.Type {
+				case diff.LineAdded:
+					if i == 0 {
+						lnum++ // Increment line number only when it's at head.
+					}
+					state.newLines = append(state.newLines, diffLine.Content)
+					state.originalLines = append(state.originalLines, buildOriginalLine(path, diffLine))
+					switch prevState {
+					case diff.LineUnchanged:
+						// Insert.
+						state.startLine = lnum + 1
+						state.isInsert = true
+					case diff.LineDeleted, diff.LineAdded:
+						// Do nothing in particular.
+					}
+				case diff.LineDeleted:
+					lnum++
+					state.originalLines = append(state.originalLines, buildOriginalLine(path, diffLine))
+					switch prevState {
+					case diff.LineUnchanged:
+						state.startLine = lnum
+					case diff.LineAdded:
+						state.isInsert = false
+					case diff.LineDeleted:
+						// Do nothing in particular.
+					}
+				case diff.LineUnchanged:
+					switch prevState {
+					case diff.LineUnchanged:
+						// Do nothing in particular.
+					case diff.LineAdded, diff.LineDeleted:
+						emit() // Output a diagnostic.
+					}
+					lnum++
+				}
+				prevState = diffLine.Type
+			}
+			if state.startLine > 0 {
+				emit() // Output a diagnostic at the end of hunk.
+			}
+		}
+	}
+	return diagnostics, nil
+}
+
+func buildOriginalLine(path string, line *diff.Line) string {
+	var (
+		lnum int
+		mark rune
+	)
+	switch line.Type {
+	case diff.LineAdded:
+		mark = '+'
+		lnum = line.LnumNew
+	case diff.LineDeleted:
+		mark = '-'
+		lnum = line.LnumOld
+	case diff.LineUnchanged:
+		mark = ' '
+		lnum = line.LnumOld
+	}
+	return fmt.Sprintf("%s:%d:%s%s", path, lnum, string(mark), line.Content)
+}
diff --git a/parser/parser.go b/parser/parser.go
index c9a126e2..a3c05c76 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -20,6 +20,7 @@ type Parser interface {
 type Option struct {
 	FormatName  string
 	Errorformat []string
+	DiffStrip   int
 }
 
 // New returns Parser based on Option.
@@ -35,6 +36,8 @@ func New(opt *Option) (Parser, error) {
 		return NewCheckStyleParser(), nil
 	case "rdjsonl":
 		return NewRDJSONLParser(), nil
+	case "diff":
+		return NewDiffParser(opt.DiffStrip), nil
 	}
 
 	// use defined errorformat


Test patch:
diff --git a/diff/testdata/gen.sh b/diff/testdata/gen.sh
index 07b2b13a..1d6a5bd8 100644
--- a/diff/testdata/gen.sh
+++ b/diff/testdata/gen.sh
@@ -12,3 +12,4 @@ git diff --no-index golint.{old,new}.go > golint.diff
 git diff --no-index empty.txt /dev/null > newline_and_empty_deleted.diff
 git diff --no-index golint.{old,new}.go >> newline_and_empty_deleted.diff
 git -c core.quotepath=true diff --no-index 日本語.{old,new}.txt > 日本語.diff
+gofmt -d gofmt.go > gofmt.diff
diff --git a/diff/testdata/gofmt.diff b/diff/testdata/gofmt.diff
new file mode 100644
index 00000000..154bb987
--- /dev/null
+++ b/diff/testdata/gofmt.diff
@@ -0,0 +1,27 @@
+diff -u gofmt.go.orig gofmt.go
+--- gofmt.go.orig	2020-07-26 08:01:09.260800318 +0000
++++ gofmt.go	2020-07-26 08:01:09.260800318 +0000
+@@ -1,6 +1,6 @@
+ package testdata
+ 
+-func    fmt     () {
++func fmt() {
+ 	// test
+ 	// test line
+ 	// test line
+@@ -10,11 +10,11 @@
+ 	// test line
+ 	// test line
+ 
+-println(
+-		"hello, gofmt test"    )
+-//comment
++	println(
++		"hello, gofmt test")
++	//comment
+ }
+ 
++type s struct{ A int }
+ 
+-type s struct { A int }
+ func (s s) String() { return "s" }
diff --git a/diff/testdata/gofmt.diff.json b/diff/testdata/gofmt.diff.json
new file mode 100644
index 00000000..4bf6ead5
--- /dev/null
+++ b/diff/testdata/gofmt.diff.json
@@ -0,0 +1,185 @@
+[
+  {
+    "PathOld": "gofmt.go.orig",
+    "PathNew": "gofmt.go",
+    "TimeOld": "2020-07-26 08:01:09.260800318 +0000",
+    "TimeNew": "2020-07-26 08:01:09.260800318 +0000",
+    "Hunks": [
+      {
+        "StartLineOld": 1,
+        "LineLengthOld": 6,
+        "StartLineNew": 1,
+        "LineLengthNew": 6,
+        "Section": "",
+        "Lines": [
+          {
+            "Type": 0,
+            "Content": "package testdata",
+            "LnumDiff": 1,
+            "LnumOld": 1,
+            "LnumNew": 1
+          },
+          {
+            "Type": 0,
+            "Content": "",
+            "LnumDiff": 2,
+            "LnumOld": 2,
+            "LnumNew": 2
+          },
+          {
+            "Type": 2,
+            "Content": "func    fmt     () {",
+            "LnumDiff": 3,
+            "LnumOld": 3,
+            "LnumNew": 0
+          },
+          {
+            "Type": 1,
+            "Content": "func fmt() {",
+            "LnumDiff": 4,
+            "LnumOld": 0,
+            "LnumNew": 3
+          },
+          {
+            "Type": 0,
+            "Content": "\t// test",
+            "LnumDiff": 5,
+            "LnumOld": 4,
+            "LnumNew": 4
+          },
+          {
+            "Type": 0,
+            "Content": "\t// test line",
+            "LnumDiff": 6,
+            "LnumOld": 5,
+            "LnumNew": 5
+          },
+          {
+            "Type": 0,
+            "Content": "\t// test line",
+            "LnumDiff": 7,
+            "LnumOld": 6,
+            "LnumNew": 6
+          }
+        ]
+      },
+      {
+        "StartLineOld": 10,
+        "LineLengthOld": 11,
+        "StartLineNew": 10,
+        "LineLengthNew": 11,
+        "Section": "",
+        "Lines": [
+          {
+            "Type": 0,
+            "Content": "\t// test line",
+            "LnumDiff": 9,
+            "LnumOld": 10,
+            "LnumNew": 10
+          },
+          {
+            "Type": 0,
+            "Content": "\t// test line",
+            "LnumDiff": 10,
+            "LnumOld": 11,
+            "LnumNew": 11
+          },
+          {
+            "Type": 0,
+            "Content": "",
+            "LnumDiff": 11,
+            "LnumOld": 12,
+            "LnumNew": 12
+          },
+          {
+            "Type": 2,
+            "Content": "println(",
+            "LnumDiff": 12,
+            "LnumOld": 13,
+            "LnumNew": 0
+          },
+          {
+            "Type": 2,
+            "Content": "\t\t\"hello, gofmt test\"    )",
+            "LnumDiff": 13,
+            "LnumOld": 14,
+            "LnumNew": 0
+          },
+          {
+            "Type": 2,
+            "Content": "//comment",
+            "LnumDiff": 14,
+            "LnumOld": 15,
+            "LnumNew": 0
+          },
+          {
+            "Type": 1,
+            "Content": "\tprintln(",
+            "LnumDiff": 15,
+            "LnumOld": 0,
+            "LnumNew": 13
+          },
+          {
+            "Type": 1,
+            "Content": "\t\t\"hello, gofmt test\")",
+            "LnumDiff": 16,
+            "LnumOld": 0,
+            "LnumNew": 14
+          },
+          {
+            "Type": 1,
+            "Content": "\t//comment",
+            "LnumDiff": 17,
+            "LnumOld": 0,
+            "LnumNew": 15
+          },
+          {
+            "Type": 0,
+            "Content": "}",
+            "LnumDiff": 18,
+            "LnumOld": 16,
+            "LnumNew": 16
+          },
+          {
+            "Type": 0,
+            "Content": "",
+            "LnumDiff": 19,
+            "LnumOld": 17,
+            "LnumNew": 17
+          },
+          {
+            "Type": 1,
+            "Content": "type s struct{ A int }",
+            "LnumDiff": 20,
+            "LnumOld": 0,
+            "LnumNew": 18
+          },
+          {
+            "Type": 0,
+            "Content": "",
+            "LnumDiff": 21,
+            "LnumOld": 18,
+            "LnumNew": 19
+          },
+          {
+            "Type": 2,
+            "Content": "type s struct { A int }",
+            "LnumDiff": 22,
+            "LnumOld": 19,
+            "LnumNew": 0
+          },
+          {
+            "Type": 0,
+            "Content": "func (s s) String() { return \"s\" }",
+            "LnumDiff": 23,
+            "LnumOld": 20,
+            "LnumNew": 20
+          }
+        ]
+      }
+    ],
+    "Extended": [
+      "diff -u gofmt.go.orig gofmt.go"
+    ]
+  }
+]
diff --git a/diff/testdata/gofmt.go b/diff/testdata/gofmt.go
new file mode 100644
index 00000000..9bd629cc
--- /dev/null
+++ b/diff/testdata/gofmt.go
@@ -0,0 +1,20 @@
+package testdata
+
+func    fmt     () {
+	// test
+	// test line
+	// test line
+	// test line
+	// test line
+	// test line
+	// test line
+	// test line
+
+println(
+		"hello, gofmt test"    )
+//comment
+}
+
+
+type s struct { A int }
+func (s s) String() { return "s" }
diff --git a/diff/testdata/nonewline.diff b/diff/testdata/nonewline.diff
index a98ad596..172278f3 100644
--- a/diff/testdata/nonewline.diff
+++ b/diff/testdata/nonewline.diff
@@ -1,5 +1,5 @@
---- nonewline.old.txt	2016-10-13 15:34:14.931778318 +0900
-+++ nonewline.new.txt	2016-10-13 15:34:14.868444672 +0900
+--- nonewline.old.txt	2019-04-19 13:19:50.331526534 +0000
++++ nonewline.new.txt	2019-04-19 13:19:50.331526534 +0000
 @@ -1,4 +1,4 @@
  " vim: nofixeol noendofline
  No newline at end of both the old and new file
diff --git a/diff/testdata/nonewline.diff.json b/diff/testdata/nonewline.diff.json
index f6c91719..3a4e6a88 100644
--- a/diff/testdata/nonewline.diff.json
+++ b/diff/testdata/nonewline.diff.json
@@ -2,8 +2,8 @@
   {
     "PathOld": "nonewline.old.txt",
     "PathNew": "nonewline.new.txt",
-    "TimeOld": "2016-10-13 15:34:14.931778318 +0900",
-    "TimeNew": "2016-10-13 15:34:14.868444672 +0900",
+    "TimeOld": "2019-04-19 13:19:50.331526534 +0000",
+    "TimeNew": "2019-04-19 13:19:50.331526534 +0000",
     "Hunks": [
       {
         "StartLineOld": 1,
diff --git a/diff/testdata/nonewline2.diff b/diff/testdata/nonewline2.diff
index 7aa74d25..e8b25641 100644
--- a/diff/testdata/nonewline2.diff
+++ b/diff/testdata/nonewline2.diff
@@ -1,5 +1,5 @@
---- nonewline2.old.txt	2016-10-13 15:34:15.141779355 +0900
-+++ nonewline2.new.txt	2016-10-13 15:34:15.075112360 +0900
+--- nonewline2.old.txt	2019-04-19 13:19:50.331526534 +0000
++++ nonewline2.new.txt	2019-04-19 13:19:50.331526534 +0000
 @@ -1,4 +1,4 @@
  No newline at end of both the old and new file (unchanged)
 -a
diff --git a/diff/testdata/nonewline2.diff.json b/diff/testdata/nonewline2.diff.json
index cd7cfaa6..dec71876 100644
--- a/diff/testdata/nonewline2.diff.json
+++ b/diff/testdata/nonewline2.diff.json
@@ -2,8 +2,8 @@
   {
     "PathOld": "nonewline2.old.txt",
     "PathNew": "nonewline2.new.txt",
-    "TimeOld": "2016-10-13 15:34:15.141779355 +0900",
-    "TimeNew": "2016-10-13 15:34:15.075112360 +0900",
+    "TimeOld": "2019-04-19 13:19:50.331526534 +0000",
+    "TimeNew": "2019-04-19 13:19:50.331526534 +0000",
     "Hunks": [
       {
         "StartLineOld": 1,
diff --git a/diff/testdata/nonewline3.diff b/diff/testdata/nonewline3.diff
index a14f0a85..1bccfd4a 100644
--- a/diff/testdata/nonewline3.diff
+++ b/diff/testdata/nonewline3.diff
@@ -1,5 +1,5 @@
---- nonewline3.old.txt	2016-10-13 15:34:15.361780443 +0900
-+++ nonewline3.new.txt	2016-10-13 15:34:15.288446747 +0900
+--- nonewline3.old.txt	2019-04-19 13:19:50.331526534 +0000
++++ nonewline3.new.txt	2019-04-19 13:19:50.331526534 +0000
 @@ -1,6 +1,7 @@
  No newline at end of the old file
  Newline at end of the new file
diff --git a/diff/testdata/nonewline3.diff.json b/diff/testdata/nonewline3.diff.json
index 3c36b12a..799cc1d3 100644
--- a/diff/testdata/nonewline3.diff.json
+++ b/diff/testdata/nonewline3.diff.json
@@ -2,8 +2,8 @@
   {
     "PathOld": "nonewline3.old.txt",
     "PathNew": "nonewline3.new.txt",
-    "TimeOld": "2016-10-13 15:34:15.361780443 +0900",
-    "TimeNew": "2016-10-13 15:34:15.288446747 +0900",
+    "TimeOld": "2019-04-19 13:19:50.331526534 +0000",
+    "TimeNew": "2019-04-19 13:19:50.331526534 +0000",
     "Hunks": [
       {
         "StartLineOld": 1,
diff --git a/diff/testdata/sample.diff b/diff/testdata/sample.diff
index 304074c2..346b2e1c 100644
--- a/diff/testdata/sample.diff
+++ b/diff/testdata/sample.diff
@@ -1,5 +1,5 @@
---- sample.old.txt	2016-10-13 05:09:35.820791185 +0900
-+++ sample.new.txt	2016-10-13 05:15:26.839245048 +0900
+--- sample.old.txt	2019-04-19 13:19:50.331526534 +0000
++++ sample.new.txt	2019-04-19 13:19:50.331526534 +0000
 @@ -1,3 +1,4 @@
  unchanged, contextual line
 -deleted line
diff --git a/diff/testdata/sample.diff.json b/diff/testdata/sample.diff.json
index 3b255703..cc341527 100644
--- a/diff/testdata/sample.diff.json
+++ b/diff/testdata/sample.diff.json
@@ -2,8 +2,8 @@
   {
     "PathOld": "sample.old.txt",
     "PathNew": "sample.new.txt",
-    "TimeOld": "2016-10-13 05:09:35.820791185 +0900",
-    "TimeNew": "2016-10-13 05:15:26.839245048 +0900",
+    "TimeOld": "2019-04-19 13:19:50.331526534 +0000",
+    "TimeNew": "2019-04-19 13:19:50.331526534 +0000",
     "Hunks": [
       {
         "StartLineOld": 1,
diff --git a/parser/diff_test.go b/parser/diff_test.go
new file mode 100644
index 00000000..fb938592
--- /dev/null
+++ b/parser/diff_test.go
@@ -0,0 +1,165 @@
+package parser
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"google.golang.org/protobuf/encoding/protojson"
+)
+
+func ExampleDiffParser() {
+	const sample = `diff --git a/gofmt.go b/gofmt.go
+--- a/gofmt.go	2020-07-26 08:01:09.260800318 +0000
++++ b/gofmt.go	2020-07-26 08:01:09.260800318 +0000
+@@ -1,6 +1,6 @@
+ package testdata
+ 
+-func    fmt     () {
++func fmt() {
+ 	// test
+ 	// test line
+ 	// test line
+@@ -10,11 +10,11 @@
+ 	// test line
+ 	// test line
+ 
+-println(
+-		"hello, gofmt test"    )
+-//comment
++	println(
++		"hello, gofmt test")
++	//comment
+ }
+ 
++type s struct{ A int }
+ 
+-type s struct { A int }
+ func (s s) String() { return "s" }
+`
+	const strip = 1
+	p := NewDiffParser(strip)
+	diagnostics, err := p.Parse(strings.NewReader(sample))
+	if err != nil {
+		panic(err)
+	}
+	for _, d := range diagnostics {
+		rdjson, _ := protojson.MarshalOptions{Indent: "  "}.Marshal(d)
+		var out bytes.Buffer
+		json.Indent(&out, rdjson, "", "  ")
+		fmt.Println(out.String())
+	}
+	// Output:
+	// {
+	//   "location": {
+	//     "path": "gofmt.go",
+	//     "range": {
+	//       "start": {
+	//         "line": 3
+	//       },
+	//       "end": {
+	//         "line": 3
+	//       }
+	//     }
+	//   },
+	//   "suggestions": [
+	//     {
+	//       "range": {
+	//         "start": {
+	//           "line": 3
+	//         },
+	//         "end": {
+	//           "line": 3
+	//         }
+	//       },
+	//       "text": "func fmt() {"
+	//     }
+	//   ],
+	//   "originalOutput": "gofmt.go:3:-func    fmt     () {\ngofmt.go:3:+func fmt() {"
+	// }
+	// {
+	//   "location": {
+	//     "path": "gofmt.go",
+	//     "range": {
+	//       "start": {
+	//         "line": 13
+	//       },
+	//       "end": {
+	//         "line": 15
+	//       }
+	//     }
+	//   },
+	//   "suggestions": [
+	//     {
+	//       "range": {
+	//         "start": {
+	//           "line": 13
+	//         },
+	//         "end": {
+	//           "line": 15
+	//         }
+	//       },
+	//       "text": "\tprintln(\n\t\t\"hello, gofmt test\")\n\t//comment"
+	//     }
+	//   ],
+	//   "originalOutput": "gofmt.go:13:-println(\ngofmt.go:14:-\t\t\"hello, gofmt test\"    )\ngofmt.go:15:-//comment\ngofmt.go:13:+\tprintln(\ngofmt.go:14:+\t\t\"hello, gofmt test\")\ngofmt.go:15:+\t//comment"
+	// }
+	// {
+	//   "location": {
+	//     "path": "gofmt.go",
+	//     "range": {
+	//       "start": {
+	//         "line": 18,
+	//         "column": 1
+	//       },
+	//       "end": {
+	//         "line": 18,
+	//         "column": 1
+	//       }
+	//     }
+	//   },
+	//   "suggestions": [
+	//     {
+	//       "range": {
+	//         "start": {
+	//           "line": 18,
+	//           "column": 1
+	//         },
+	//         "end": {
+	//           "line": 18,
+	//           "column": 1
+	//         }
+	//       },
+	//       "text": "type s struct{ A int }\n"
+	//     }
+	//   ],
+	//   "originalOutput": "gofmt.go:18:+type s struct{ A int }"
+	// }
+	// {
+	//   "location": {
+	//     "path": "gofmt.go",
+	//     "range": {
+	//       "start": {
+	//         "line": 19
+	//       },
+	//       "end": {
+	//         "line": 19
+	//       }
+	//     }
+	//   },
+	//   "suggestions": [
+	//     {
+	//       "range": {
+	//         "start": {
+	//           "line": 19
+	//         },
+	//         "end": {
+	//           "line": 19
+	//         }
+	//       }
+	//     }
+	//   ],
+	//   "originalOutput": "gofmt.go:19:-type s struct { A int }"
+	// }
+}