Repository: reviewdog/reviewdog
Pull request #690 (2020-07-25 10:44:51)

refactor: create parser package
- [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/cmd/reviewdog/main.go b/cmd/reviewdog/main.go
index 4abb1e16..0d6d0c98 100644
--- a/cmd/reviewdog/main.go
+++ b/cmd/reviewdog/main.go
@@ -28,6 +28,7 @@ import (
 	"github.com/reviewdog/reviewdog/cienv"
 	"github.com/reviewdog/reviewdog/commands"
 	"github.com/reviewdog/reviewdog/difffilter"
+	"github.com/reviewdog/reviewdog/parser"
 	"github.com/reviewdog/reviewdog/project"
 	gerritservice "github.com/reviewdog/reviewdog/service/gerrit"
 	githubservice "github.com/reviewdog/reviewdog/service/github"
@@ -668,8 +669,8 @@ func readConf(conf string) ([]byte, error) {
 	return nil, errors.New(".reviewdog.yml not found")
 }
 
-func newParserFromOpt(opt *option) (reviewdog.Parser, error) {
-	p, err := reviewdog.NewParser(&reviewdog.ParserOpt{FormatName: opt.f, Errorformat: opt.efms})
+func newParserFromOpt(opt *option) (parser.Parser, error) {
+	p, err := parser.New(&parser.Option{FormatName: opt.f, Errorformat: opt.efms})
 	if err != nil {
 		return nil, fmt.Errorf("fail to create parser. use either -f or -efm: %w", err)
 	}
diff --git a/parser.go b/parser.go
deleted file mode 100644
index 8bb1d4c2..00000000
--- a/parser.go
+++ /dev/null
@@ -1,185 +0,0 @@
-package reviewdog
-
-import (
-	"bufio"
-	"encoding/xml"
-	"errors"
-	"fmt"
-	"io"
-	"strings"
-
-	"github.com/reviewdog/errorformat"
-	"github.com/reviewdog/errorformat/fmts"
-	"github.com/reviewdog/reviewdog/proto/rdf"
-	"google.golang.org/protobuf/encoding/protojson"
-)
-
-// ParserOpt represents option to create Parser. Either FormatName or
-// Errorformat should be specified.
-type ParserOpt struct {
-	FormatName  string
-	Errorformat []string
-}
-
-// NewParser returns Parser based on ParserOpt.
-func NewParser(opt *ParserOpt) (Parser, error) {
-	name := opt.FormatName
-
-	if name != "" && len(opt.Errorformat) > 0 {
-		return nil, errors.New("you cannot specify both format name and errorformat at the same time")
-	}
-
-	switch name {
-	case "checkstyle":
-		return NewCheckStyleParser(), nil
-	case "rdjsonl":
-		return NewRDJSONLParser(), nil
-	}
-
-	// use defined errorformat
-	if name != "" {
-		efm, ok := fmts.DefinedFmts()[name]
-		if !ok {
-			return nil, fmt.Errorf("%q is not supported. consider to add new errorformat to https://github.com/reviewdog/errorformat", name)
-		}
-		opt.Errorformat = efm.Errorformat
-	}
-	if len(opt.Errorformat) == 0 {
-		return nil, errors.New("errorformat is empty")
-	}
-	return NewErrorformatParserString(opt.Errorformat)
-}
-
-var _ Parser = &ErrorformatParser{}
-
-// ErrorformatParser is errorformat parser.
-type ErrorformatParser struct {
-	efm *errorformat.Errorformat
-}
-
-// NewErrorformatParser returns a new ErrorformatParser.
-func NewErrorformatParser(efm *errorformat.Errorformat) *ErrorformatParser {
-	return &ErrorformatParser{efm: efm}
-}
-
-// NewErrorformatParserString returns a new ErrorformatParser from errorformat
-// in string representation.
-func NewErrorformatParserString(efms []string) (*ErrorformatParser, error) {
-	efm, err := errorformat.NewErrorformat(efms)
-	if err != nil {
-		return nil, err
-	}
-	return NewErrorformatParser(efm), nil
-}
-
-func (p *ErrorformatParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
-	s := p.efm.NewScanner(r)
-	var rs []*rdf.Diagnostic
-	for s.Scan() {
-		e := s.Entry()
-		if e.Valid {
-			rs = append(rs, &rdf.Diagnostic{
-				Location: &rdf.Location{
-					Path: e.Filename,
-					Range: &rdf.Range{
-						Start: &rdf.Position{
-							Line:   int32(e.Lnum),
-							Column: int32(e.Col),
-						},
-					},
-				},
-				Message:        e.Text,
-				OriginalOutput: strings.Join(e.Lines, "\n"),
-			})
-		}
-	}
-	return rs, nil
-}
-
-var _ Parser = &CheckStyleParser{}
-
-// CheckStyleParser is checkstyle parser.
-type CheckStyleParser struct{}
-
-// NewCheckStyleParser returns a new CheckStyleParser.
-func NewCheckStyleParser() Parser {
-	return &CheckStyleParser{}
-}
-
-func (p *CheckStyleParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
-	var cs = new(CheckStyleResult)
-	if err := xml.NewDecoder(r).Decode(cs); err != nil {
-		return nil, err
-	}
-	var rs []*rdf.Diagnostic
-	for _, file := range cs.Files {
-		for _, cerr := range file.Errors {
-			rs = append(rs, &rdf.Diagnostic{
-				Location: &rdf.Location{
-					Path: file.Name,
-					Range: &rdf.Range{
-						Start: &rdf.Position{
-							Line:   int32(cerr.Line),
-							Column: int32(cerr.Column),
-						},
-					},
-				},
-				Message: cerr.Message,
-				OriginalOutput: fmt.Sprintf("%v:%d:%d: %v: %v (%v)",
-					file.Name, cerr.Line, cerr.Column, cerr.Severity, cerr.Message, cerr.Source),
-			})
-		}
-	}
-	return rs, nil
-}
-
-// CheckStyleResult represents checkstyle XML result.
-// <?xml version="1.0" encoding="utf-8"?><checkstyle version="4.3"><file ...></file>...</checkstyle>
-//
-// References:
-//   - http://checkstyle.sourceforge.net/
-//   - http://eslint.org/docs/user-guide/formatters/#checkstyle
-type CheckStyleResult struct {
-	XMLName xml.Name          `xml:"checkstyle"`
-	Version string            `xml:"version,attr"`
-	Files   []*CheckStyleFile `xml:"file,omitempty"`
-}
-
-// CheckStyleFile represents <file name="fname"><error ... />...</file>
-type CheckStyleFile struct {
-	Name   string             `xml:"name,attr"`
-	Errors []*CheckStyleError `xml:"error"`
-}
-
-// CheckStyleError represents <error line="1" column="10" severity="error" message="msg" source="src" />
-type CheckStyleError struct {
-	Column   int    `xml:"column,attr,omitempty"`
-	Line     int    `xml:"line,attr"`
-	Message  string `xml:"message,attr"`
-	Severity string `xml:"severity,attr,omitempty"`
-	Source   string `xml:"source,attr,omitempty"`
-}
-
-// RDJSONLParser is parser for rdjsonl format.
-type RDJSONLParser struct{}
-
-func NewRDJSONLParser() *RDJSONLParser {
-	return &RDJSONLParser{}
-}
-
-func (p *RDJSONLParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
-	var results []*rdf.Diagnostic
-	s := bufio.NewScanner(r)
-	for s.Scan() {
-		d := new(rdf.Diagnostic)
-		if err := protojson.Unmarshal(s.Bytes(), d); err != nil {
-			return nil, err
-		}
-		if d.GetOriginalOutput() == "" {
-			// TODO(haya14busa): Refactor not to fill in original output.
-			d.OriginalOutput = s.Text()
-		}
-		results = append(results, d)
-	}
-	return results, nil
-}
diff --git a/parser/checkstyle.go b/parser/checkstyle.go
new file mode 100644
index 00000000..dbd6be4c
--- /dev/null
+++ b/parser/checkstyle.go
@@ -0,0 +1,73 @@
+package parser
+
+import (
+	"encoding/xml"
+	"fmt"
+	"io"
+
+	"github.com/reviewdog/reviewdog/proto/rdf"
+)
+
+var _ Parser = &CheckStyleParser{}
+
+// CheckStyleParser is checkstyle parser.
+type CheckStyleParser struct{}
+
+// NewCheckStyleParser returns a new CheckStyleParser.
+func NewCheckStyleParser() Parser {
+	return &CheckStyleParser{}
+}
+
+func (p *CheckStyleParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
+	var cs = new(CheckStyleResult)
+	if err := xml.NewDecoder(r).Decode(cs); err != nil {
+		return nil, err
+	}
+	var rs []*rdf.Diagnostic
+	for _, file := range cs.Files {
+		for _, cerr := range file.Errors {
+			rs = append(rs, &rdf.Diagnostic{
+				Location: &rdf.Location{
+					Path: file.Name,
+					Range: &rdf.Range{
+						Start: &rdf.Position{
+							Line:   int32(cerr.Line),
+							Column: int32(cerr.Column),
+						},
+					},
+				},
+				Message: cerr.Message,
+				OriginalOutput: fmt.Sprintf("%v:%d:%d: %v: %v (%v)",
+					file.Name, cerr.Line, cerr.Column, cerr.Severity, cerr.Message, cerr.Source),
+			})
+		}
+	}
+	return rs, nil
+}
+
+// CheckStyleResult represents checkstyle XML result.
+// <?xml version="1.0" encoding="utf-8"?><checkstyle version="4.3"><file ...></file>...</checkstyle>
+//
+// References:
+//   - http://checkstyle.sourceforge.net/
+//   - http://eslint.org/docs/user-guide/formatters/#checkstyle
+type CheckStyleResult struct {
+	XMLName xml.Name          `xml:"checkstyle"`
+	Version string            `xml:"version,attr"`
+	Files   []*CheckStyleFile `xml:"file,omitempty"`
+}
+
+// CheckStyleFile represents <file name="fname"><error ... />...</file>
+type CheckStyleFile struct {
+	Name   string             `xml:"name,attr"`
+	Errors []*CheckStyleError `xml:"error"`
+}
+
+// CheckStyleError represents <error line="1" column="10" severity="error" message="msg" source="src" />
+type CheckStyleError struct {
+	Column   int    `xml:"column,attr,omitempty"`
+	Line     int    `xml:"line,attr"`
+	Message  string `xml:"message,attr"`
+	Severity string `xml:"severity,attr,omitempty"`
+	Source   string `xml:"source,attr,omitempty"`
+}
diff --git a/parser/errorformat.go b/parser/errorformat.go
new file mode 100644
index 00000000..44fbd7c7
--- /dev/null
+++ b/parser/errorformat.go
@@ -0,0 +1,55 @@
+package parser
+
+import (
+	"io"
+	"strings"
+
+	"github.com/reviewdog/errorformat"
+	"github.com/reviewdog/reviewdog/proto/rdf"
+)
+
+var _ Parser = &ErrorformatParser{}
+
+// ErrorformatParser is errorformat parser.
+type ErrorformatParser struct {
+	efm *errorformat.Errorformat
+}
+
+// NewErrorformatParser returns a new ErrorformatParser.
+func NewErrorformatParser(efm *errorformat.Errorformat) *ErrorformatParser {
+	return &ErrorformatParser{efm: efm}
+}
+
+// NewErrorformatParserString returns a new ErrorformatParser from errorformat
+// in string representation.
+func NewErrorformatParserString(efms []string) (*ErrorformatParser, error) {
+	efm, err := errorformat.NewErrorformat(efms)
+	if err != nil {
+		return nil, err
+	}
+	return NewErrorformatParser(efm), nil
+}
+
+func (p *ErrorformatParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
+	s := p.efm.NewScanner(r)
+	var rs []*rdf.Diagnostic
+	for s.Scan() {
+		e := s.Entry()
+		if e.Valid {
+			rs = append(rs, &rdf.Diagnostic{
+				Location: &rdf.Location{
+					Path: e.Filename,
+					Range: &rdf.Range{
+						Start: &rdf.Position{
+							Line:   int32(e.Lnum),
+							Column: int32(e.Col),
+						},
+					},
+				},
+				Message:        e.Text,
+				OriginalOutput: strings.Join(e.Lines, "\n"),
+			})
+		}
+	}
+	return rs, nil
+}
diff --git a/parser/parser.go b/parser/parser.go
new file mode 100644
index 00000000..1406c15a
--- /dev/null
+++ b/parser/parser.go
@@ -0,0 +1,52 @@
+package parser
+
+import (
+	"errors"
+	"fmt"
+	"io"
+
+	"github.com/reviewdog/errorformat/fmts"
+	"github.com/reviewdog/reviewdog/proto/rdf"
+)
+
+// Parser is an interface which parses compilers, linters, or any tools
+// results.
+type Parser interface {
+	Parse(r io.Reader) ([]*rdf.Diagnostic, error)
+}
+
+// Option represents option to create Parser. Either FormatName or
+// Errorformat should be specified.
+type Option struct {
+	FormatName  string
+	Errorformat []string
+}
+
+// New returns Parser based on Option.
+func New(opt *Option) (Parser, error) {
+	name := opt.FormatName
+
+	if name != "" && len(opt.Errorformat) > 0 {
+		return nil, errors.New("you cannot specify both format name and errorformat at the same time")
+	}
+
+	switch name {
+	case "checkstyle":
+		return NewCheckStyleParser(), nil
+	case "rdjsonl":
+		return NewRDJSONLParser(), nil
+	}
+
+	// use defined errorformat
+	if name != "" {
+		efm, ok := fmts.DefinedFmts()[name]
+		if !ok {
+			return nil, fmt.Errorf("%q is not supported. consider to add new errorformat to https://github.com/reviewdog/errorformat", name)
+		}
+		opt.Errorformat = efm.Errorformat
+	}
+	if len(opt.Errorformat) == 0 {
+		return nil, errors.New("errorformat is empty")
+	}
+	return NewErrorformatParserString(opt.Errorformat)
+}
diff --git a/parser/rdjsonl.go b/parser/rdjsonl.go
new file mode 100644
index 00000000..d4065f8f
--- /dev/null
+++ b/parser/rdjsonl.go
@@ -0,0 +1,35 @@
+package parser
+
+import (
+	"bufio"
+	"io"
+
+	"github.com/reviewdog/reviewdog/proto/rdf"
+	"google.golang.org/protobuf/encoding/protojson"
+)
+
+var _ Parser = &RDJSONLParser{}
+
+// RDJSONLParser is parser for rdjsonl format.
+type RDJSONLParser struct{}
+
+func NewRDJSONLParser() *RDJSONLParser {
+	return &RDJSONLParser{}
+}
+
+func (p *RDJSONLParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
+	var results []*rdf.Diagnostic
+	s := bufio.NewScanner(r)
+	for s.Scan() {
+		d := new(rdf.Diagnostic)
+		if err := protojson.Unmarshal(s.Bytes(), d); err != nil {
+			return nil, err
+		}
+		if d.GetOriginalOutput() == "" {
+			// TODO(haya14busa): Refactor not to fill in original output.
+			d.OriginalOutput = s.Text()
+		}
+		results = append(results, d)
+	}
+	return results, nil
+}
diff --git a/project/run.go b/project/run.go
index c904719e..90a6128d 100644
--- a/project/run.go
+++ b/project/run.go
@@ -15,6 +15,7 @@ import (
 	"github.com/reviewdog/reviewdog"
 	"github.com/reviewdog/reviewdog/diff"
 	"github.com/reviewdog/reviewdog/difffilter"
+	"github.com/reviewdog/reviewdog/parser"
 )
 
 // RunAndParse runs commands and parse results. Returns map of tool name to check results.
@@ -43,8 +44,8 @@ func RunAndParse(ctx context.Context, conf *Config, runners map[string]bool, def
 		if fname == "" && len(runner.Errorformat) == 0 {
 			fname = runnerName
 		}
-		opt := &reviewdog.ParserOpt{FormatName: fname, Errorformat: runner.Errorformat}
-		p, err := reviewdog.NewParser(opt)
+		opt := &parser.Option{FormatName: fname, Errorformat: runner.Errorformat}
+		p, err := parser.New(opt)
 		if err != nil {
 			return nil, err
 		}
diff --git a/reviewdog.go b/reviewdog.go
index d3038b0d..96a232e1 100644
--- a/reviewdog.go
+++ b/reviewdog.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/reviewdog/reviewdog/diff"
 	"github.com/reviewdog/reviewdog/difffilter"
+	"github.com/reviewdog/reviewdog/parser"
 	"github.com/reviewdog/reviewdog/proto/rdf"
 )
 
@@ -17,7 +18,7 @@ import (
 // results.
 type Reviewdog struct {
 	toolname    string
-	p           Parser
+	p           parser.Parser
 	c           CommentService
 	d           DiffService
 	filterMode  difffilter.Mode
@@ -25,7 +26,7 @@ type Reviewdog struct {
 }
 
 // NewReviewdog returns a new Reviewdog.
-func NewReviewdog(toolname string, p Parser, c CommentService, d DiffService, filterMode difffilter.Mode, failOnError bool) *Reviewdog {
+func NewReviewdog(toolname string, p parser.Parser, c CommentService, d DiffService, filterMode difffilter.Mode, failOnError bool) *Reviewdog {
 	return &Reviewdog{p: p, c: c, d: d, toolname: toolname, filterMode: filterMode, failOnError: failOnError}
 }
 
@@ -35,12 +36,6 @@ func RunFromResult(ctx context.Context, c CommentService, results []*rdf.Diagnos
 	return (&Reviewdog{c: c, toolname: toolname, filterMode: filterMode, failOnError: failOnError}).runFromResult(ctx, results, filediffs, strip, failOnError)
 }
 
-// Parser is an interface which parses compilers, linters, or any tools
-// results.
-type Parser interface {
-	Parse(r io.Reader) ([]*rdf.Diagnostic, error)
-}
-
 // Comment represents a reported result as a comment.
 type Comment struct {
 	Result   *FilteredCheck
@@ -123,3 +118,4 @@ func (w *Reviewdog) Run(ctx context.Context, r io.Reader) error {
 
 	return w.runFromResult(ctx, results, filediffs, w.d.Strip(), w.failOnError)
 }
+


Test patch:
diff --git a/parser/checkstyle_test.go b/parser/checkstyle_test.go
new file mode 100644
index 00000000..4ffa16fc
--- /dev/null
+++ b/parser/checkstyle_test.go
@@ -0,0 +1,34 @@
+package parser
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestCheckStyleParser(t *testing.T) {
+	const sample = `<?xml version="1.0" encoding="utf-8"?><checkstyle version="4.3"><file name="/path/to/file"><error line="1" column="10" severity="error" message="&apos;addOne&apos; is defined but never used. (no-unused-vars)" source="eslint.rules.no-unused-vars" /><error line="2" column="9" severity="error" message="Use the isNaN function to compare with NaN. (use-isnan)" source="eslint.rules.use-isnan" /><error line="3" column="16" severity="error" message="Unexpected space before unary operator &apos;++&apos;. (space-unary-ops)" source="eslint.rules.space-unary-ops" /><error line="3" column="20" severity="warning" message="Missing semicolon. (semi)" source="eslint.rules.semi" /><error line="4" column="12" severity="warning" message="Unnecessary &apos;else&apos; after &apos;return&apos;. (no-else-return)" source="eslint.rules.no-else-return" /><error line="5" column="7" severity="warning" message="Expected indentation of 8 spaces but found 6. (indent)" source="eslint.rules.indent" /><error line="5" column="7" severity="error" message="Expected a return value. (consistent-return)" source="eslint.rules.consistent-return" /><error line="5" column="13" severity="warning" message="Missing semicolon. (semi)" source="eslint.rules.semi" /><error line="7" column="2" severity="error" message="Unnecessary semicolon. (no-extra-semi)" source="eslint.rules.no-extra-semi" /></file></checkstyle>`
+
+	wants := []string{
+
+		"/path/to/file:1:10: error: 'addOne' is defined but never used. (no-unused-vars) (eslint.rules.no-unused-vars)",
+		"/path/to/file:2:9: error: Use the isNaN function to compare with NaN. (use-isnan) (eslint.rules.use-isnan)",
+		"/path/to/file:3:16: error: Unexpected space before unary operator '++'. (space-unary-ops) (eslint.rules.space-unary-ops)",
+		"/path/to/file:3:20: warning: Missing semicolon. (semi) (eslint.rules.semi)",
+		"/path/to/file:4:12: warning: Unnecessary 'else' after 'return'. (no-else-return) (eslint.rules.no-else-return)",
+		"/path/to/file:5:7: warning: Expected indentation of 8 spaces but found 6. (indent) (eslint.rules.indent)",
+		"/path/to/file:5:7: error: Expected a return value. (consistent-return) (eslint.rules.consistent-return)",
+		"/path/to/file:5:13: warning: Missing semicolon. (semi) (eslint.rules.semi)",
+		"/path/to/file:7:2: error: Unnecessary semicolon. (no-extra-semi) (eslint.rules.no-extra-semi)",
+	}
+
+	p := NewCheckStyleParser()
+	diagnostics, err := p.Parse(strings.NewReader(sample))
+	if err != nil {
+		t.Error(err)
+	}
+	for i, d := range diagnostics {
+		if got, want := d.GetOriginalOutput(), wants[i]; got != want {
+			t.Errorf("%d: got %v, want %v", i, got, want)
+		}
+	}
+}
diff --git a/parser/errorformat_test.go b/parser/errorformat_test.go
new file mode 100644
index 00000000..f3dff0b6
--- /dev/null
+++ b/parser/errorformat_test.go
@@ -0,0 +1,16 @@
+package parser
+
+import "testing"
+
+func TestNewErrorformatParserString(t *testing.T) {
+	in := []string{`%f:%l:%c:%m`, `%-G%.%#`}
+
+	got, err := NewErrorformatParserString(in)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if len(got.efm.Efms) != len(in) {
+		t.Errorf("NewErrorformatParserString: len: got %v, want %v", len(got.efm.Efms), len(in))
+	}
+}
diff --git a/parser/parser_test.go b/parser/parser_test.go
new file mode 100644
index 00000000..960ccb90
--- /dev/null
+++ b/parser/parser_test.go
@@ -0,0 +1,69 @@
+package parser
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestNewParser(t *testing.T) {
+	tests := []struct {
+		in      *Option
+		typ     Parser
+		wantErr bool
+	}{
+		{
+			in: &Option{
+				FormatName: "checkstyle",
+			},
+			typ: &CheckStyleParser{},
+		},
+		{
+			in: &Option{
+				FormatName: "rdjsonl",
+			},
+			typ: &RDJSONLParser{},
+		},
+		{
+			in: &Option{
+				FormatName: "golint",
+			},
+			typ: &ErrorformatParser{},
+		},
+		{
+			in: &Option{
+				Errorformat: []string{`%f:%l:%c:%m`},
+			},
+			typ: &ErrorformatParser{},
+		},
+		{ // empty
+			in:      &Option{},
+			wantErr: true,
+		},
+		{ // both
+			in: &Option{
+				FormatName:  "checkstyle",
+				Errorformat: []string{`%f:%l:%c:%m`},
+			},
+			wantErr: true,
+		},
+		{ // unsupported
+			in: &Option{
+				FormatName: "unsupported format",
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		p, err := New(tt.in)
+		if tt.wantErr && err != nil {
+			continue
+		}
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+		if got, want := reflect.TypeOf(p), reflect.TypeOf(tt.typ); got != want {
+			t.Errorf("typ: got %v, want %v", got, want)
+		}
+	}
+}
diff --git a/parser/rdjsonl_test.go b/parser/rdjsonl_test.go
new file mode 100644
index 00000000..cb701cf9
--- /dev/null
+++ b/parser/rdjsonl_test.go
@@ -0,0 +1,27 @@
+package parser
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestRDJSONLParser(t *testing.T) {
+	const sample = `{"source":{"name":"deadcode"},"message":"'unused' is unused","location":{"path":"testdata/main.go","range":{"start":{"line":18,"column":6}}}}
+{"source":{"name":"deadcode"},"message":"'unused2' is unused","location":{"path":"testdata/main.go","range":{"start":{"line":24,"column":6}}}}
+{"source":{"name":"errcheck"},"message":"Error return value of 'os.Open' is not checked","location":{"path":"testdata/main.go","range":{"start":{"line":15,"column":9}}}}
+{"source":{"name":"ineffassign"},"message":"ineffectual assignment to 'x'","location":{"path":"testdata/main.go","range":{"start":{"line":12,"column":2}}}}
+{"source":{"name":"govet"},"message":"printf: Sprintf format %d reads arg #1, but call has 0 args","location":{"path":"testdata/main.go","range":{"start":{"line":13,"column":2}}}}
+{"source":{"name":"severity-test"},"message":"severity test (string)","location":{"path":"testdata/main.go","range":{"start":{"line":24,"column":6}}}, "severity": "WARNING"}
+{"source":{"name":"severity-test"},"message":"severity test (number)","location":{"path":"testdata/main.go","range":{"start":{"line":24,"column":6}}}, "severity": "WARNING"}`
+	sampleLines := strings.Split(sample, "\n")
+	p := NewRDJSONLParser()
+	diagnostics, err := p.Parse(strings.NewReader(sample))
+	if err != nil {
+		t.Error(err)
+	}
+	for i, d := range diagnostics {
+		if got, want := d.GetOriginalOutput(), sampleLines[i]; got != want {
+			t.Errorf("%d: got %v, want %v", i, got, want)
+		}
+	}
+}
diff --git a/parser_test.go b/parser_test.go
deleted file mode 100644
index b7f56470..00000000
--- a/parser_test.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package reviewdog
-
-import (
-	"reflect"
-	"strings"
-	"testing"
-)
-
-func TestNewParser(t *testing.T) {
-	tests := []struct {
-		in      *ParserOpt
-		typ     Parser
-		wantErr bool
-	}{
-		{
-			in: &ParserOpt{
-				FormatName: "checkstyle",
-			},
-			typ: &CheckStyleParser{},
-		},
-		{
-			in: &ParserOpt{
-				FormatName: "rdjsonl",
-			},
-			typ: &RDJSONLParser{},
-		},
-		{
-			in: &ParserOpt{
-				FormatName: "golint",
-			},
-			typ: &ErrorformatParser{},
-		},
-		{
-			in: &ParserOpt{
-				Errorformat: []string{`%f:%l:%c:%m`},
-			},
-			typ: &ErrorformatParser{},
-		},
-		{ // empty
-			in:      &ParserOpt{},
-			wantErr: true,
-		},
-		{ // both
-			in: &ParserOpt{
-				FormatName:  "checkstyle",
-				Errorformat: []string{`%f:%l:%c:%m`},
-			},
-			wantErr: true,
-		},
-		{ // unsupported
-			in: &ParserOpt{
-				FormatName: "unsupported format",
-			},
-			wantErr: true,
-		},
-	}
-	for _, tt := range tests {
-		p, err := NewParser(tt.in)
-		if tt.wantErr && err != nil {
-			continue
-		}
-		if err != nil {
-			t.Error(err)
-			continue
-		}
-		if got, want := reflect.TypeOf(p), reflect.TypeOf(tt.typ); got != want {
-			t.Errorf("typ: got %v, want %v", got, want)
-		}
-	}
-}
-
-func TestNewErrorformatParserString(t *testing.T) {
-	in := []string{`%f:%l:%c:%m`, `%-G%.%#`}
-
-	got, err := NewErrorformatParserString(in)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if len(got.efm.Efms) != len(in) {
-		t.Errorf("NewErrorformatParserString: len: got %v, want %v", len(got.efm.Efms), len(in))
-	}
-}
-
-func TestCheckStyleParser(t *testing.T) {
-	const sample = `<?xml version="1.0" encoding="utf-8"?><checkstyle version="4.3"><file name="/path/to/file"><error line="1" column="10" severity="error" message="&apos;addOne&apos; is defined but never used. (no-unused-vars)" source="eslint.rules.no-unused-vars" /><error line="2" column="9" severity="error" message="Use the isNaN function to compare with NaN. (use-isnan)" source="eslint.rules.use-isnan" /><error line="3" column="16" severity="error" message="Unexpected space before unary operator &apos;++&apos;. (space-unary-ops)" source="eslint.rules.space-unary-ops" /><error line="3" column="20" severity="warning" message="Missing semicolon. (semi)" source="eslint.rules.semi" /><error line="4" column="12" severity="warning" message="Unnecessary &apos;else&apos; after &apos;return&apos;. (no-else-return)" source="eslint.rules.no-else-return" /><error line="5" column="7" severity="warning" message="Expected indentation of 8 spaces but found 6. (indent)" source="eslint.rules.indent" /><error line="5" column="7" severity="error" message="Expected a return value. (consistent-return)" source="eslint.rules.consistent-return" /><error line="5" column="13" severity="warning" message="Missing semicolon. (semi)" source="eslint.rules.semi" /><error line="7" column="2" severity="error" message="Unnecessary semicolon. (no-extra-semi)" source="eslint.rules.no-extra-semi" /></file></checkstyle>`
-
-	wants := []string{
-
-		"/path/to/file:1:10: error: 'addOne' is defined but never used. (no-unused-vars) (eslint.rules.no-unused-vars)",
-		"/path/to/file:2:9: error: Use the isNaN function to compare with NaN. (use-isnan) (eslint.rules.use-isnan)",
-		"/path/to/file:3:16: error: Unexpected space before unary operator '++'. (space-unary-ops) (eslint.rules.space-unary-ops)",
-		"/path/to/file:3:20: warning: Missing semicolon. (semi) (eslint.rules.semi)",
-		"/path/to/file:4:12: warning: Unnecessary 'else' after 'return'. (no-else-return) (eslint.rules.no-else-return)",
-		"/path/to/file:5:7: warning: Expected indentation of 8 spaces but found 6. (indent) (eslint.rules.indent)",
-		"/path/to/file:5:7: error: Expected a return value. (consistent-return) (eslint.rules.consistent-return)",
-		"/path/to/file:5:13: warning: Missing semicolon. (semi) (eslint.rules.semi)",
-		"/path/to/file:7:2: error: Unnecessary semicolon. (no-extra-semi) (eslint.rules.no-extra-semi)",
-	}
-
-	p := NewCheckStyleParser()
-	diagnostics, err := p.Parse(strings.NewReader(sample))
-	if err != nil {
-		t.Error(err)
-	}
-	for i, d := range diagnostics {
-		if got, want := d.GetOriginalOutput(), wants[i]; got != want {
-			t.Errorf("%d: got %v, want %v", i, got, want)
-		}
-	}
-}
-
-func TestRDJSONLParser(t *testing.T) {
-	const sample = `{"source":{"name":"deadcode"},"message":"'unused' is unused","location":{"path":"testdata/main.go","range":{"start":{"line":18,"column":6}}}}
-{"source":{"name":"deadcode"},"message":"'unused2' is unused","location":{"path":"testdata/main.go","range":{"start":{"line":24,"column":6}}}}
-{"source":{"name":"errcheck"},"message":"Error return value of 'os.Open' is not checked","location":{"path":"testdata/main.go","range":{"start":{"line":15,"column":9}}}}
-{"source":{"name":"ineffassign"},"message":"ineffectual assignment to 'x'","location":{"path":"testdata/main.go","range":{"start":{"line":12,"column":2}}}}
-{"source":{"name":"govet"},"message":"printf: Sprintf format %d reads arg #1, but call has 0 args","location":{"path":"testdata/main.go","range":{"start":{"line":13,"column":2}}}}
-{"source":{"name":"severity-test"},"message":"severity test (string)","location":{"path":"testdata/main.go","range":{"start":{"line":24,"column":6}}}, "severity": "WARNING"}
-{"source":{"name":"severity-test"},"message":"severity test (number)","location":{"path":"testdata/main.go","range":{"start":{"line":24,"column":6}}}, "severity": "WARNING"}`
-	sampleLines := strings.Split(sample, "\n")
-	p := NewRDJSONLParser()
-	diagnostics, err := p.Parse(strings.NewReader(sample))
-	if err != nil {
-		t.Error(err)
-	}
-	for i, d := range diagnostics {
-		if got, want := d.GetOriginalOutput(), sampleLines[i]; got != want {
-			t.Errorf("%d: got %v, want %v", i, got, want)
-		}
-	}
-}
diff --git a/reviewdog_test.go b/reviewdog_test.go
index 92e0f874..f53f7f86 100644
--- a/reviewdog_test.go
+++ b/reviewdog_test.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/reviewdog/errorformat"
 	"github.com/reviewdog/reviewdog/difffilter"
+	"github.com/reviewdog/reviewdog/parser"
 )
 
 var _ CommentService = &testWriter{}
@@ -45,7 +46,7 @@ golint.new.go:7:1: comment on exported function F should be of the form "F ..."
 golint.new.go:11:1: comment on exported function F2 should be of the form "F2 ..."
 `
 	efm, _ := errorformat.NewErrorformat([]string{`%f:%l:%c: %m`})
-	p := NewErrorformatParser(efm)
+	p := parser.NewErrorformatParser(efm)
 	c := NewRawCommentWriter(os.Stdout)
 	d := NewDiffString(difftext, 1)
 	app := NewReviewdog("tool name", p, c, d, difffilter.ModeAdded, false)
@@ -92,7 +93,7 @@ index 34cacb9..a727dd3 100644
 	}
 
 	efm, _ := errorformat.NewErrorformat([]string{`%f:%l:%c: %m`})
-	p := NewErrorformatParser(efm)
+	p := parser.NewErrorformatParser(efm)
 	d := NewDiffString(difftext, 1)
 	app := NewReviewdog("tool name", p, c, d, difffilter.ModeAdded, false)
 	app.Run(context.Background(), strings.NewReader(lintresult))
@@ -125,7 +126,7 @@ golint.new.go:11:1: comment on exported function F2 should be of the form "F2 ..
 
 	c := NewRawCommentWriter(os.Stdout)
 	efm, _ := errorformat.NewErrorformat([]string{`%f:%l:%c: %m`})
-	p := NewErrorformatParser(efm)
+	p := parser.NewErrorformatParser(efm)
 	d := NewDiffString(difftext, 1)
 	app := NewReviewdog("tool name", p, c, d, difffilter.ModeAdded, false)
 	err := app.Run(context.Background(), strings.NewReader(lintresult))
@@ -161,7 +162,7 @@ golint.new.go:11:1: comment on exported function F2 should be of the form "F2 ..
 `
 	c := NewRawCommentWriter(os.Stdout)
 	efm, _ := errorformat.NewErrorformat([]string{`%f:%l:%c: %m`})
-	p := NewErrorformatParser(efm)
+	p := parser.NewErrorformatParser(efm)
 	d := NewDiffString(difftext, 1)
 	app := NewReviewdog("tool name", p, c, d, difffilter.ModeAdded, true)
 	err := app.Run(context.Background(), strings.NewReader(lintresult))