From e7df875d245ef964ae199a25511e66fbd27a6837 Mon Sep 17 00:00:00 2001
From: Boming Zhang <bomingzh@sjtu.edu.cn>
Date: Sat, 8 Jun 2024 20:43:46 -0400
Subject: [PATCH 1/5] feat: elf parser only parse json output to struct

---
 internal/parsers/elf/meta.go   |  9 ++++
 internal/parsers/elf/model.go  | 41 +++++++++++++++
 internal/parsers/elf/parser.go | 96 ++++++++++++++++++++++++++++++++++
 3 files changed, 146 insertions(+)
 create mode 100644 internal/parsers/elf/meta.go
 create mode 100644 internal/parsers/elf/model.go
 create mode 100644 internal/parsers/elf/parser.go

diff --git a/internal/parsers/elf/meta.go b/internal/parsers/elf/meta.go
new file mode 100644
index 0000000..0557a3c
--- /dev/null
+++ b/internal/parsers/elf/meta.go
@@ -0,0 +1,9 @@
+package elf
+
+import "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage"
+
+var name = "elf"
+
+func init() {
+	stage.RegisterParser(name, &Elf{})
+}
diff --git a/internal/parsers/elf/model.go b/internal/parsers/elf/model.go
new file mode 100644
index 0000000..a8e7960
--- /dev/null
+++ b/internal/parsers/elf/model.go
@@ -0,0 +1,41 @@
+package elf
+
+type Toplevel struct {
+	Title   string   `json:"title"`
+	Modules []Module `json:"modules"`
+}
+
+type Module struct {
+	Entries   []Entry `json:"entries"`
+	DebugInfo string  `json:"debug_info"`
+}
+
+type Entry []any
+
+type Report struct {
+	File  string `json:"file"`
+	Name  string `json:"name"`
+	Cases []Case `json:"cases" mapstructure:"cases"`
+}
+
+type Case struct {
+	Binders        []Binder `mapstructure:"binders"`
+	Context        *string  `mapstructure:"context"`
+	Depths         *int     `mapstructure:"depths"`
+	Code           *string  `mapstructure:"code"`
+	Plain          *int     `mapstructure:"plain"`
+	Weighed        *float64 `mapstructure:"weighed"`
+	Detail         *string  `mapstructure:"detail"`
+	SimilarityRate float64  `mapstructure:"similarity_rate"`
+	Sources        []Source `mapstructure:"srcs"`
+}
+
+type Binder struct {
+	Binder string `json:"binder"`
+	Pos    string `json:"pos"`
+}
+
+type Source struct {
+	Context string `json:"context"`
+	Code    string `json:"code"`
+}
diff --git a/internal/parsers/elf/parser.go b/internal/parsers/elf/parser.go
new file mode 100644
index 0000000..96e4abd
--- /dev/null
+++ b/internal/parsers/elf/parser.go
@@ -0,0 +1,96 @@
+package elf
+
+import (
+	"encoding/json"
+	"fmt"
+	"log/slog"
+
+	"focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage"
+	"github.com/criyle/go-judge/envexec"
+	"github.com/mitchellh/mapstructure"
+)
+
+type Conf struct {
+	Score   int
+	Comment string
+}
+
+type Elf struct{}
+
+func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
+	stdout := executorResult.Files["stdout"]
+	stderr := executorResult.Files["stderr"]
+	if executorResult.Status != stage.Status(envexec.StatusAccepted) {
+		return stage.ParserResult{
+			Score: 0,
+			Comment: fmt.Sprintf(
+				"Unexpected executor status: %s.\nStderr: %s",
+				executorResult.Status, stderr,
+			),
+		}
+	}
+	var topLevel Toplevel
+	err := json.Unmarshal([]byte(stdout), &topLevel)
+	if err != nil {
+		return stage.ParserResult{
+			Score:   0,
+			Comment: fmt.Sprintf("Failed to parse result: %s", err),
+		}
+	}
+	for _, module := range topLevel.Modules {
+		for _, entry := range module.Entries {
+			kind := entry[0].(string)
+			report := Report{}
+			err := mapstructure.Decode(entry[1], &report)
+			if err != nil {
+				slog.Error("elf parse", "mapstructure decode err", err)
+			}
+			slog.Debug("elf parse", "report file", report.File)
+			slog.Debug("elf parse", "report name", report.Name)
+			slog.Debug("elf parse", "report kind", kind)
+			for _, caseObj := range report.Cases {
+				switch kind {
+				case "ParenDep":
+					slog.Debug("elf parse", "binders", caseObj.Binders)
+					slog.Debug("elf parse", "context", caseObj.Context)
+					slog.Debug("elf parse", "depths", caseObj.Depths)
+					slog.Debug("elf parse", "code", caseObj.Code)
+				case "CodeLen":
+					slog.Debug("elf parse", "binders", caseObj.Binders)
+					slog.Debug("elf parse", "context", caseObj.Context)
+					slog.Debug("elf parse", "plain", caseObj.Plain)
+					slog.Debug("elf parse", "weighed", caseObj.Weighed)
+					slog.Debug("elf parse", "code", caseObj.Code)
+				case "OverArity":
+					slog.Debug("elf parse", "binders", caseObj.Binders)
+					slog.Debug("elf parse", "context", caseObj.Context)
+					slog.Debug("elf parse", "detail", caseObj.Detail)
+					slog.Debug("elf parse", "code", caseObj.Code)
+				case "CodeDup":
+					slog.Debug("elf parse", "similarity rate", caseObj.SimilarityRate)
+					for _, source := range caseObj.Sources {
+						slog.Debug("elf parse", "context", source.Context, "code", source.Code)
+					}
+				}
+			}
+		}
+	}
+	return stage.ParserResult{
+		Score:   conf.Score,
+		Comment: conf.Comment,
+	}
+}
+
+func (*Elf) Run(results []stage.ExecutorResult, confAny any) (
+	[]stage.ParserResult, bool, error,
+) {
+	conf, err := stage.DecodeConf[Conf](confAny)
+	if err != nil {
+		return nil, true, err
+	}
+	var res []stage.ParserResult
+	for _, result := range results {
+		res = append(res, Parse(result, *conf))
+	}
+	return res, false, nil
+}
-- 
2.30.2


From 0bfedd462528679b479f0af6d6939f3077ae1df5 Mon Sep 17 00:00:00 2001
From: Boming Zhang <bomingzh@sjtu.edu.cn>
Date: Sat, 15 Jun 2024 01:01:57 -0400
Subject: [PATCH 2/5] fix: remove pointer in elf `Case`

---
 internal/parsers/elf/model.go | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/internal/parsers/elf/model.go b/internal/parsers/elf/model.go
index a8e7960..28806c4 100644
--- a/internal/parsers/elf/model.go
+++ b/internal/parsers/elf/model.go
@@ -20,12 +20,12 @@ type Report struct {
 
 type Case struct {
 	Binders        []Binder `mapstructure:"binders"`
-	Context        *string  `mapstructure:"context"`
-	Depths         *int     `mapstructure:"depths"`
-	Code           *string  `mapstructure:"code"`
-	Plain          *int     `mapstructure:"plain"`
-	Weighed        *float64 `mapstructure:"weighed"`
-	Detail         *string  `mapstructure:"detail"`
+	Context        string   `mapstructure:"context"`
+	Depths         int      `mapstructure:"depths"`
+	Code           string   `mapstructure:"code"`
+	Plain          int      `mapstructure:"plain"`
+	Weighed        float64  `mapstructure:"weighed"`
+	Detail         string   `mapstructure:"detail"`
 	SimilarityRate float64  `mapstructure:"similarity_rate"`
 	Sources        []Source `mapstructure:"srcs"`
 }
-- 
2.30.2


From b57e3f81cafd480cd6edb6afedc175914abc863d Mon Sep 17 00:00:00 2001
From: Boming Zhang <bomingzh@sjtu.edu.cn>
Date: Fri, 4 Oct 2024 05:20:55 -0400
Subject: [PATCH 3/5] chore: fix module name

---
 internal/parsers/elf/meta.go   | 2 +-
 internal/parsers/elf/parser.go | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/internal/parsers/elf/meta.go b/internal/parsers/elf/meta.go
index 0557a3c..d952f2b 100644
--- a/internal/parsers/elf/meta.go
+++ b/internal/parsers/elf/meta.go
@@ -1,6 +1,6 @@
 package elf
 
-import "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage"
+import "github.com/joint-online-judge/JOJ3/internal/stage"
 
 var name = "elf"
 
diff --git a/internal/parsers/elf/parser.go b/internal/parsers/elf/parser.go
index 96e4abd..db5477a 100644
--- a/internal/parsers/elf/parser.go
+++ b/internal/parsers/elf/parser.go
@@ -5,8 +5,8 @@ import (
 	"fmt"
 	"log/slog"
 
-	"focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage"
 	"github.com/criyle/go-judge/envexec"
+	"github.com/joint-online-judge/JOJ3/internal/stage"
 	"github.com/mitchellh/mapstructure"
 )
 
-- 
2.30.2


From a4232884c5bd8ab3e189f38f287eccf8d7b62295 Mon Sep 17 00:00:00 2001
From: Boming Zhang <bomingzh@sjtu.edu.cn>
Date: Sat, 24 May 2025 04:18:16 -0400
Subject: [PATCH 4/5] refactor(parser/elf): modern structure

---
 internal/{parsers => parser}/elf/meta.go   |  7 +++++++
 internal/{parsers => parser}/elf/model.go  |  0
 internal/{parsers => parser}/elf/parser.go | 12 ++----------
 3 files changed, 9 insertions(+), 10 deletions(-)
 rename internal/{parsers => parser}/elf/meta.go (67%)
 rename internal/{parsers => parser}/elf/model.go (100%)
 rename internal/{parsers => parser}/elf/parser.go (92%)

diff --git a/internal/parsers/elf/meta.go b/internal/parser/elf/meta.go
similarity index 67%
rename from internal/parsers/elf/meta.go
rename to internal/parser/elf/meta.go
index d952f2b..ed52fc7 100644
--- a/internal/parsers/elf/meta.go
+++ b/internal/parser/elf/meta.go
@@ -4,6 +4,13 @@ import "github.com/joint-online-judge/JOJ3/internal/stage"
 
 var name = "elf"
 
+type Conf struct {
+	Score   int
+	Comment string
+}
+
+type Elf struct{}
+
 func init() {
 	stage.RegisterParser(name, &Elf{})
 }
diff --git a/internal/parsers/elf/model.go b/internal/parser/elf/model.go
similarity index 100%
rename from internal/parsers/elf/model.go
rename to internal/parser/elf/model.go
diff --git a/internal/parsers/elf/parser.go b/internal/parser/elf/parser.go
similarity index 92%
rename from internal/parsers/elf/parser.go
rename to internal/parser/elf/parser.go
index db5477a..a876700 100644
--- a/internal/parsers/elf/parser.go
+++ b/internal/parser/elf/parser.go
@@ -5,22 +5,14 @@ import (
 	"fmt"
 	"log/slog"
 
-	"github.com/criyle/go-judge/envexec"
 	"github.com/joint-online-judge/JOJ3/internal/stage"
 	"github.com/mitchellh/mapstructure"
 )
 
-type Conf struct {
-	Score   int
-	Comment string
-}
-
-type Elf struct{}
-
 func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
 	stdout := executorResult.Files["stdout"]
 	stderr := executorResult.Files["stderr"]
-	if executorResult.Status != stage.Status(envexec.StatusAccepted) {
+	if executorResult.Status != stage.StatusAccepted {
 		return stage.ParserResult{
 			Score: 0,
 			Comment: fmt.Sprintf(
@@ -88,7 +80,7 @@ func (*Elf) Run(results []stage.ExecutorResult, confAny any) (
 	if err != nil {
 		return nil, true, err
 	}
-	var res []stage.ParserResult
+	res := make([]stage.ParserResult, 0, len(results))
 	for _, result := range results {
 		res = append(res, Parse(result, *conf))
 	}
-- 
2.30.2


From 3dfdf5a2573836cf151e95b7a1ce46b196ec1f00 Mon Sep 17 00:00:00 2001
From: Boming Zhang <bomingzh@sjtu.edu.cn>
Date: Sat, 24 May 2025 04:47:45 -0400
Subject: [PATCH 5/5] feat(parser/elf): score & comment

---
 internal/parser/elf/meta.go   |  15 ++++-
 internal/parser/elf/model.go  |  21 ++++++-
 internal/parser/elf/parser.go | 112 ++++++++++++++++++++++------------
 3 files changed, 107 insertions(+), 41 deletions(-)

diff --git a/internal/parser/elf/meta.go b/internal/parser/elf/meta.go
index ed52fc7..8c37314 100644
--- a/internal/parser/elf/meta.go
+++ b/internal/parser/elf/meta.go
@@ -1,12 +1,23 @@
+// Package elf parses output of the elf static analysis tool to
+// assign scores based on detected code issues.
+
 package elf
 
 import "github.com/joint-online-judge/JOJ3/internal/stage"
 
 var name = "elf"
 
+type Match struct {
+	Keywords []string
+	Score    int
+}
+
 type Conf struct {
-	Score   int
-	Comment string
+	Score             int
+	Matches           []Match
+	Stdout            string `default:"stdout"`
+	Stderr            string `default:"stderr"`
+	ForceQuitOnDeduct bool   `default:"false"`
 }
 
 type Elf struct{}
diff --git a/internal/parser/elf/model.go b/internal/parser/elf/model.go
index 28806c4..f998543 100644
--- a/internal/parser/elf/model.go
+++ b/internal/parser/elf/model.go
@@ -1,5 +1,10 @@
 package elf
 
+import (
+	"fmt"
+	"strings"
+)
+
 type Toplevel struct {
 	Title   string   `json:"title"`
 	Modules []Module `json:"modules"`
@@ -19,7 +24,7 @@ type Report struct {
 }
 
 type Case struct {
-	Binders        []Binder `mapstructure:"binders"`
+	Binders        Binders  `mapstructure:"binders"`
 	Context        string   `mapstructure:"context"`
 	Depths         int      `mapstructure:"depths"`
 	Code           string   `mapstructure:"code"`
@@ -35,6 +40,20 @@ type Binder struct {
 	Pos    string `json:"pos"`
 }
 
+func (b Binder) String() string {
+	return fmt.Sprintf("In the definition of %s (at %s)", b.Binder, b.Pos)
+}
+
+type Binders []Binder
+
+func (bs Binders) String() string {
+	s := make([]string, 0, len(bs))
+	for _, b := range bs {
+		s = append(s, b.String())
+	}
+	return strings.Join(s, "; ")
+}
+
 type Source struct {
 	Context string `json:"context"`
 	Code    string `json:"code"`
diff --git a/internal/parser/elf/parser.go b/internal/parser/elf/parser.go
index a876700..2d2eaea 100644
--- a/internal/parser/elf/parser.go
+++ b/internal/parser/elf/parser.go
@@ -4,31 +4,28 @@ import (
 	"encoding/json"
 	"fmt"
 	"log/slog"
+	"strings"
 
 	"github.com/joint-online-judge/JOJ3/internal/stage"
 	"github.com/mitchellh/mapstructure"
 )
 
-func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
-	stdout := executorResult.Files["stdout"]
-	stderr := executorResult.Files["stderr"]
-	if executorResult.Status != stage.StatusAccepted {
-		return stage.ParserResult{
-			Score: 0,
-			Comment: fmt.Sprintf(
-				"Unexpected executor status: %s.\nStderr: %s",
-				executorResult.Status, stderr,
-			),
-		}
-	}
+func (p *Elf) parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
+	stdout := executorResult.Files[conf.Stdout]
+	// stderr := executorResult.Files[conf.Stderr]
 	var topLevel Toplevel
 	err := json.Unmarshal([]byte(stdout), &topLevel)
 	if err != nil {
 		return stage.ParserResult{
-			Score:   0,
-			Comment: fmt.Sprintf("Failed to parse result: %s", err),
+			Score: 0,
+			Comment: fmt.Sprintf(
+				"Unexpected parser error: %s.",
+				err,
+			),
 		}
 	}
+	score := conf.Score
+	comment := ""
 	for _, module := range topLevel.Modules {
 		for _, entry := range module.Entries {
 			kind := entry[0].(string)
@@ -37,43 +34,77 @@ func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
 			if err != nil {
 				slog.Error("elf parse", "mapstructure decode err", err)
 			}
-			slog.Debug("elf parse", "report file", report.File)
-			slog.Debug("elf parse", "report name", report.Name)
-			slog.Debug("elf parse", "report kind", kind)
+			comment += fmt.Sprintf("### [%s] %s\n", report.File, report.Name)
 			for _, caseObj := range report.Cases {
+				for _, match := range conf.Matches {
+					for _, keyword := range match.Keywords {
+						if strings.Contains(kind, keyword) {
+							score += -match.Score
+						}
+					}
+				}
 				switch kind {
 				case "ParenDep":
-					slog.Debug("elf parse", "binders", caseObj.Binders)
-					slog.Debug("elf parse", "context", caseObj.Context)
-					slog.Debug("elf parse", "depths", caseObj.Depths)
-					slog.Debug("elf parse", "code", caseObj.Code)
+					// "<binders>:\n<context> below reaches a parentheses depths of <depths>:\n<code>"
+					comment += fmt.Sprintf(
+						"%s:\n%s below reaches a parentheses depths of %d:\n"+
+							"```%s\n```\n",
+						caseObj.Binders,
+						caseObj.Context,
+						caseObj.Depths,
+						caseObj.Code,
+					)
 				case "CodeLen":
-					slog.Debug("elf parse", "binders", caseObj.Binders)
-					slog.Debug("elf parse", "context", caseObj.Context)
-					slog.Debug("elf parse", "plain", caseObj.Plain)
-					slog.Debug("elf parse", "weighed", caseObj.Weighed)
-					slog.Debug("elf parse", "code", caseObj.Code)
+					// "<binders>:\n<context> below excceeds a code length upper bound with <plain> (weighed: <weighed>):\n<code>"
+					comment += fmt.Sprintf(
+						"%s:\n%s below excceeds a code length "+
+							"upper bound with %d (weighed: %f):\n"+
+							"```%s\n```\n",
+						caseObj.Binders,
+						caseObj.Context,
+						caseObj.Plain,
+						caseObj.Weighed,
+						caseObj.Code,
+					)
 				case "OverArity":
-					slog.Debug("elf parse", "binders", caseObj.Binders)
-					slog.Debug("elf parse", "context", caseObj.Context)
-					slog.Debug("elf parse", "detail", caseObj.Detail)
-					slog.Debug("elf parse", "code", caseObj.Code)
+					// "<binders>:\n<context> below hits <detail>:\n<code>"
+					comment += fmt.Sprintf(
+						"%s:\n%s below hits %s:\n```%s\n```\n",
+						caseObj.Binders,
+						caseObj.Context,
+						caseObj.Detail,
+						caseObj.Code,
+					)
 				case "CodeDup":
-					slog.Debug("elf parse", "similarity rate", caseObj.SimilarityRate)
-					for _, source := range caseObj.Sources {
-						slog.Debug("elf parse", "context", source.Context, "code", source.Code)
+					if len(caseObj.Sources) != 2 {
+						slog.Error("elf parse", "code dup sources length", len(caseObj.Sources))
 					}
+					context0 := caseObj.Sources[0].Context
+					code0 := caseObj.Sources[0].Code
+					context1 := caseObj.Sources[1].Context
+					code1 := caseObj.Sources[1].Code
+					// "The code below has a similarity rate of <similarity_rate>:\n- <context1>:\n\t<code1>\n- <context2>:\n\t<code2>"
+					comment += fmt.Sprintf(
+						"The code below has a similarity rate of %f:\n"+
+							"- %s:\n```%s\n```\n"+
+							"- %s:\n```%s\n```\n",
+						caseObj.SimilarityRate,
+						context0,
+						code0,
+						context1,
+						code1,
+					)
 				}
 			}
 		}
 	}
 	return stage.ParserResult{
-		Score:   conf.Score,
-		Comment: conf.Comment,
+		Score:   score,
+		Comment: comment,
 	}
 }
 
-func (*Elf) Run(results []stage.ExecutorResult, confAny any) (
+func (p *Elf) Run(results []stage.ExecutorResult, confAny any) (
 	[]stage.ParserResult, bool, error,
 ) {
 	conf, err := stage.DecodeConf[Conf](confAny)
@@ -81,8 +112,13 @@ func (*Elf) Run(results []stage.ExecutorResult, confAny any) (
 		return nil, true, err
 	}
 	res := make([]stage.ParserResult, 0, len(results))
+	forceQuit := false
 	for _, result := range results {
-		res = append(res, Parse(result, *conf))
+		parseRes := p.parse(result, *conf)
+		if conf.ForceQuitOnDeduct && parseRes.Score < conf.Score {
+			forceQuit = true
+		}
+		res = append(res, parseRes)
 	}
-	return res, false, nil
+	return res, forceQuit, nil
 }
-- 
2.30.2