feat(parser/elf): score & comment
All checks were successful
build / build (pull_request) Successful in 6m40s
build / build (push) Successful in 6m42s
build / trigger-build-image (push) Has been skipped
build / trigger-build-image (pull_request) Has been skipped

This commit is contained in:
张泊明518370910136 2025-05-24 04:47:45 -04:00
parent a4232884c5
commit 3dfdf5a257
GPG Key ID: D47306D7062CDA9D
3 changed files with 107 additions and 41 deletions

View File

@ -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{}

View File

@ -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"`

View File

@ -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
}