Compare commits
No commits in common. "a3a0d99be65c43ad6fc4a59f819e2ee0b35c57e8" and "f3a93a209707a2fe30a9bdc69023fffeb4ba8a9a" have entirely different histories.
a3a0d99be6
...
f3a93a2097
|
@ -1,28 +1,12 @@
|
|||
linters:
|
||||
enable:
|
||||
- bidichk
|
||||
- dupl
|
||||
- errcheck
|
||||
- gocritic
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- staticcheck
|
||||
- stylecheck
|
||||
- testifylint
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- unparam
|
||||
- usetesting
|
||||
- wastedassign
|
||||
linters-settings:
|
||||
stylecheck:
|
||||
checks: ["all", "-ST1005"]
|
||||
- gocritic
|
||||
- gofumpt
|
||||
- gofmt
|
||||
- goimports
|
||||
issues:
|
||||
exclude-rules:
|
||||
- linters:
|
||||
- gosec
|
||||
text: G601 # false positive since go 1.22
|
||||
|
|
2
cmd/joj3/env/env.go
vendored
2
cmd/joj3/env/env.go
vendored
|
@ -42,7 +42,7 @@ func init() {
|
|||
low := timestamp & 0xFFFFFFFF
|
||||
combined := high ^ low
|
||||
combined ^= int64(pid)
|
||||
combined ^= timestamp >> 16
|
||||
combined ^= int64(timestamp >> 16)
|
||||
combined ^= (combined >> 8)
|
||||
combined ^= (combined << 16)
|
||||
Attr.RunID = fmt.Sprintf("%08X", combined&0xFFFFFFFF)
|
||||
|
|
|
@ -66,12 +66,12 @@ func newSlogAttrs(csvPath string) (attrs []slog.Attr) {
|
|||
}
|
||||
// if csvPath is empty, just return
|
||||
if csvPath == "" {
|
||||
return attrs
|
||||
return
|
||||
}
|
||||
file, err := os.Open(csvPath)
|
||||
if err != nil {
|
||||
slog.Error("open csv", "error", err)
|
||||
return attrs
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
reader := csv.NewReader(file)
|
||||
|
@ -82,7 +82,7 @@ func newSlogAttrs(csvPath string) (attrs []slog.Attr) {
|
|||
}
|
||||
if err != nil {
|
||||
slog.Error("read csv", "error", err)
|
||||
return attrs
|
||||
return
|
||||
}
|
||||
if len(row) < 3 {
|
||||
continue
|
||||
|
@ -103,7 +103,7 @@ func newSlogAttrs(csvPath string) (attrs []slog.Attr) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return attrs
|
||||
return
|
||||
}
|
||||
|
||||
func setupSlog(conf *conf.Conf) error {
|
||||
|
@ -121,14 +121,14 @@ func setupSlog(conf *conf.Conf) error {
|
|||
&slog.HandlerOptions{Level: slog.LevelDebug})
|
||||
handlers = append(handlers, debugTextHandler.WithAttrs(attrs))
|
||||
// Json file handler for debug logs
|
||||
debugJSONFile, err := os.OpenFile(logPath+".ndjson",
|
||||
debugJsonFile, err := os.OpenFile(logPath+".ndjson",
|
||||
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
debugJSONHandler := slog.NewJSONHandler(debugJSONFile,
|
||||
debugJsonHandler := slog.NewJSONHandler(debugJsonFile,
|
||||
&slog.HandlerOptions{Level: slog.LevelDebug})
|
||||
handlers = append(handlers, debugJSONHandler.WithAttrs(attrs))
|
||||
handlers = append(handlers, debugJsonHandler.WithAttrs(attrs))
|
||||
}
|
||||
stderrLogLevel := slog.LevelInfo
|
||||
if runningTest {
|
||||
|
|
|
@ -88,7 +88,20 @@ func TestRun(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt, func(t *testing.T) {
|
||||
t.Chdir(fmt.Sprintf("%s%s", root, tt))
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.Chdir(fmt.Sprintf("%s%s", root, tt))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
err := os.Chdir(origDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
os.Args = []string{"./joj3"}
|
||||
outputFile := "joj3_result.json"
|
||||
defer os.Remove(outputFile)
|
||||
|
|
|
@ -149,19 +149,19 @@ func runStages(
|
|||
if err != nil {
|
||||
slog.Error("generate preStages", "error", err)
|
||||
stageResults, forceQuitStageName = newErrorStageResults(err)
|
||||
return stageResults, forceQuitStageName, err
|
||||
return
|
||||
}
|
||||
stages, err := generateStages(conf.Stage.Stages, groups)
|
||||
if err != nil {
|
||||
slog.Error("generate stages", "error", err)
|
||||
stageResults, forceQuitStageName = newErrorStageResults(err)
|
||||
return stageResults, forceQuitStageName, err
|
||||
return
|
||||
}
|
||||
postStages, err := generateStages(conf.Stage.PostStages, groups)
|
||||
if err != nil {
|
||||
slog.Error("generate postStages", "error", err)
|
||||
stageResults, forceQuitStageName = newErrorStageResults(err)
|
||||
return stageResults, forceQuitStageName, err
|
||||
return
|
||||
}
|
||||
defer stage.Cleanup()
|
||||
// ignore force quit in preStages & postStages
|
||||
|
@ -194,5 +194,5 @@ func runStages(
|
|||
if err != nil {
|
||||
slog.Error("run postStages", "error", err)
|
||||
}
|
||||
return stageResults, forceQuitStageName, err
|
||||
return
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package dummy
|
|||
import "github.com/joint-online-judge/JOJ3/internal/stage"
|
||||
|
||||
func (e *Dummy) Run(cmds []stage.Cmd) ([]stage.ExecutorResult, error) {
|
||||
res := make([]stage.ExecutorResult, 0, len(cmds))
|
||||
var res []stage.ExecutorResult
|
||||
for range cmds {
|
||||
res = append(res, stage.ExecutorResult{
|
||||
Status: stage.StatusAccepted,
|
||||
|
|
|
@ -113,7 +113,7 @@ func ToRlimit(cmd stage.Cmd) ([]syscall.Rlimit, []int, error) {
|
|||
if err := syscall.Getrlimit(syscall.RLIMIT_CPU, ¤t); err != nil {
|
||||
return nil, nil, fmt.Errorf("getrlimit RLIMIT_CPU failed: %w", err)
|
||||
}
|
||||
userTimeLimit := min((cmd.CPULimit+1e9-1)/1e9, current.Max) // ns to s
|
||||
userTimeLimit := min((uint64(cmd.CPULimit)+1e9-1)/1e9, current.Max) // ns to s
|
||||
rlimits = append(rlimits, syscall.Rlimit{
|
||||
Cur: userTimeLimit,
|
||||
Max: current.Max,
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
// copied from https://github.com/criyle/go-judge/blob/master/cmd/go-judge-shell/grpc.go
|
||||
func convertPBCmd(cmd []stage.Cmd) []*pb.Request_CmdType {
|
||||
ret := make([]*pb.Request_CmdType, 0, len(cmd))
|
||||
var ret []*pb.Request_CmdType
|
||||
for _, c := range cmd {
|
||||
ret = append(ret, &pb.Request_CmdType{
|
||||
Args: c.Args,
|
||||
|
@ -140,7 +140,7 @@ func convertPBFile(i stage.CmdFile) *pb.Request_File {
|
|||
}
|
||||
|
||||
func convertPBResult(res []*pb.Response_Result) []stage.ExecutorResult {
|
||||
ret := make([]stage.ExecutorResult, 0, len(res))
|
||||
var ret []stage.ExecutorResult
|
||||
for _, r := range res {
|
||||
ret = append(ret, stage.ExecutorResult{
|
||||
Status: stage.Status(r.Status),
|
||||
|
@ -167,7 +167,7 @@ func convertFiles(buf map[string][]byte) map[string]string {
|
|||
}
|
||||
|
||||
func convertPBFileError(fe []*pb.Response_FileError) []stage.FileError {
|
||||
ret := make([]stage.FileError, 0, len(fe))
|
||||
var ret []stage.FileError
|
||||
for _, v := range fe {
|
||||
ret = append(ret, stage.FileError{
|
||||
Name: v.Name,
|
||||
|
|
|
@ -131,12 +131,12 @@ func convertPathsToRelative(messages *[]ClangMessage, conf Conf) {
|
|||
func parseLines(lines []string, conf Conf) []ClangMessage {
|
||||
messages := make([]ClangMessage, 0)
|
||||
for _, line := range lines {
|
||||
if isIgnored(line) {
|
||||
if isIgnored(string(line)) {
|
||||
continue
|
||||
}
|
||||
message := parseMessage(line)
|
||||
message := parseMessage(string(line))
|
||||
if message.level == UNKNOWN && len(messages) > 0 {
|
||||
messages[len(messages)-1].detailsLines = append(messages[len(messages)-1].detailsLines, line)
|
||||
messages[len(messages)-1].detailsLines = append(messages[len(messages)-1].detailsLines, string(line))
|
||||
} else if message.level != UNKNOWN {
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
)
|
||||
|
||||
// Referenced from https://github.com/yuriisk/clang-tidy-converter/blob/master/clang_tidy_converter/parser/clang_tidy_parser.py
|
||||
type JSONMessage struct {
|
||||
type JsonMessage struct {
|
||||
Type string `json:"type"`
|
||||
CheckName string `json:"checkname"`
|
||||
Description string `json:"description"`
|
||||
|
@ -17,16 +17,16 @@ type JSONMessage struct {
|
|||
Severity string `json:"severity"`
|
||||
}
|
||||
|
||||
func format(messages []ClangMessage) []JSONMessage {
|
||||
formattedMessages := make([]JSONMessage, len(messages))
|
||||
func format(messages []ClangMessage) []JsonMessage {
|
||||
formattedMessages := make([]JsonMessage, len(messages))
|
||||
for i, message := range messages {
|
||||
formattedMessages[i] = formatMessage(message)
|
||||
}
|
||||
return formattedMessages
|
||||
}
|
||||
|
||||
func formatMessage(message ClangMessage) JSONMessage {
|
||||
result := JSONMessage{
|
||||
func formatMessage(message ClangMessage) JsonMessage {
|
||||
result := JsonMessage{
|
||||
Type: "issue",
|
||||
CheckName: message.diagnosticName,
|
||||
Description: message.message,
|
||||
|
|
|
@ -26,7 +26,7 @@ func (p *ClangTidy) Run(results []stage.ExecutorResult, confAny any) (
|
|||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
res := make([]stage.ParserResult, 0, len(results))
|
||||
var res []stage.ParserResult
|
||||
forceQuit := false
|
||||
for _, result := range results {
|
||||
parseRes := p.parse(result, *conf)
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
func getResult(jsonMessages []JSONMessage, conf Conf) (int, string) {
|
||||
func getResult(jsonMessages []JsonMessage, conf Conf) (int, string) {
|
||||
score := conf.Score
|
||||
comment := "### Test results summary\n\n"
|
||||
matchCount := make(map[string]int)
|
||||
|
@ -29,7 +29,7 @@ func getResult(jsonMessages []JSONMessage, conf Conf) (int, string) {
|
|||
Count int
|
||||
ScoreChange int
|
||||
}
|
||||
results := make([]Result, 0, len(matchCount))
|
||||
var results []Result
|
||||
for keyword, count := range matchCount {
|
||||
results = append(results, Result{
|
||||
Keyword: keyword,
|
||||
|
|
|
@ -14,7 +14,7 @@ type Record struct {
|
|||
Column int `json:"column"`
|
||||
Severity string `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
ID string `json:"id"`
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
func (*CppCheck) parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
|
||||
|
@ -39,7 +39,16 @@ func (*CppCheck) parse(executorResult stage.ExecutorResult, conf Conf) stage.Par
|
|||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
comment, score := getResult(records, conf)
|
||||
comment, score, err := getResult(records, conf)
|
||||
if err != nil {
|
||||
return stage.ParserResult{
|
||||
Score: 0,
|
||||
Comment: fmt.Sprintf(
|
||||
"Unexpected parser error: %s.",
|
||||
err,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return stage.ParserResult{
|
||||
Score: score,
|
||||
|
@ -54,7 +63,7 @@ func (p *CppCheck) Run(results []stage.ExecutorResult, confAny any) (
|
|||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
res := make([]stage.ParserResult, 0, len(results))
|
||||
var res []stage.ParserResult
|
||||
forceQuit := false
|
||||
for _, result := range results {
|
||||
parseRes := p.parse(result, *conf)
|
||||
|
|
|
@ -19,7 +19,7 @@ const (
|
|||
UNKNOWN
|
||||
)
|
||||
|
||||
func getResult(records []Record, conf Conf) (string, int) {
|
||||
func getResult(records []Record, conf Conf) (string, int, error) {
|
||||
score := conf.Score
|
||||
comment := "### Test results summary\n\n"
|
||||
matchCount := make(map[string]int)
|
||||
|
@ -27,7 +27,7 @@ func getResult(records []Record, conf Conf) (string, int) {
|
|||
for _, record := range records {
|
||||
for _, match := range conf.Matches {
|
||||
for _, keyword := range match.Keywords {
|
||||
if strings.Contains(record.ID, keyword) ||
|
||||
if strings.Contains(record.Id, keyword) ||
|
||||
strings.Contains(record.Severity, keyword) {
|
||||
matchCount[keyword] += 1
|
||||
scoreChange[keyword] += -match.Score
|
||||
|
@ -41,7 +41,7 @@ func getResult(records []Record, conf Conf) (string, int) {
|
|||
Count int
|
||||
ScoreChange int
|
||||
}
|
||||
results := make([]Result, 0, len(matchCount))
|
||||
var results []Result
|
||||
for keyword, count := range matchCount {
|
||||
results = append(results, Result{
|
||||
Keyword: keyword,
|
||||
|
@ -62,5 +62,5 @@ func getResult(records []Record, conf Conf) (string, int) {
|
|||
comment += fmt.Sprintf("%d. `%s`: %d occurrence(s), %d point(s)\n",
|
||||
i+1, result.Keyword, result.Count, result.ScoreChange)
|
||||
}
|
||||
return comment, score
|
||||
return comment, score, nil
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ func (*Cpplint) parse(executorResult stage.ExecutorResult, conf Conf) stage.Pars
|
|||
Count int
|
||||
ScoreChange int
|
||||
}
|
||||
results := make([]Result, 0, len(matchCount))
|
||||
var results []Result
|
||||
for keyword, count := range matchCount {
|
||||
results = append(results, Result{
|
||||
Keyword: keyword,
|
||||
|
@ -79,7 +79,7 @@ func (p *Cpplint) Run(results []stage.ExecutorResult, confAny any) (
|
|||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
res := make([]stage.ParserResult, 0, len(results))
|
||||
var res []stage.ParserResult
|
||||
forceQuit := false
|
||||
for _, result := range results {
|
||||
parseRes := p.parse(result, *conf)
|
||||
|
|
|
@ -25,7 +25,7 @@ func (p *Debug) Run(results []stage.ExecutorResult, confAny any) (
|
|||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
res := make([]stage.ParserResult, 0, len(results))
|
||||
var res []stage.ParserResult
|
||||
for _, result := range results {
|
||||
res = append(res, p.parse(result, *conf))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package diff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// compareStrings compares two strings character by character, optionally ignoring whitespace.
|
||||
func compareStrings(str1, str2 string, compareSpace bool) bool {
|
||||
if compareSpace {
|
||||
|
@ -44,3 +49,33 @@ func isWhitespace(b byte) bool {
|
|||
b == 0x85 ||
|
||||
b == 0xA0
|
||||
}
|
||||
|
||||
func formatDiff(oldList []string, newList []string, ops []Op[string]) string {
|
||||
var result []string
|
||||
i, j := 0, 0
|
||||
for _, op := range ops {
|
||||
if op.OpType == OpDelete {
|
||||
for i < op.OldPos {
|
||||
result = append(result, " "+oldList[i])
|
||||
i++
|
||||
j++
|
||||
}
|
||||
result = append(result, "- "+fmt.Sprint(op.Elem))
|
||||
i++
|
||||
} else if op.OpType == OpInsert {
|
||||
for j < op.NewPos {
|
||||
result = append(result, " "+newList[j])
|
||||
i++
|
||||
j++
|
||||
}
|
||||
result = append(result, "+ "+fmt.Sprint(op.Elem))
|
||||
j++
|
||||
}
|
||||
}
|
||||
for i < len(oldList) && j < len(newList) {
|
||||
result = append(result, " "+oldList[i])
|
||||
i++
|
||||
j++
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
|
134
internal/parser/diff/myers.go
Normal file
134
internal/parser/diff/myers.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package diff
|
||||
|
||||
// source: https://github.com/MFAshby/myers
|
||||
// Myer's diff algorithm in golang
|
||||
// Ported from https://blog.robertelder.org/diff-algorithm/
|
||||
|
||||
type OpType int
|
||||
|
||||
const (
|
||||
OpInsert OpType = iota
|
||||
OpDelete
|
||||
)
|
||||
|
||||
type Op[T any] struct {
|
||||
OpType OpType // Insert or delete, as above
|
||||
OldPos int // Position in the old list of item to be inserted or deleted
|
||||
NewPos int // Position in the _new_ list of item to be inserted
|
||||
Elem T // Actual value to be inserted or deleted
|
||||
}
|
||||
|
||||
// Returns a minimal list of differences between 2 lists e and f
|
||||
// requiring O(min(len(e),len(f))) space and O(min(len(e),len(f)) * D)
|
||||
// worst-case execution time where D is the number of differences.
|
||||
func myersDiff[T any](e, f []T, equals func(T, T) bool) []Op[T] {
|
||||
return diffInternal(e, f, equals, 0, 0)
|
||||
}
|
||||
|
||||
func diffInternal[T any](e, f []T, equals func(T, T) bool, i, j int) []Op[T] {
|
||||
N := len(e)
|
||||
M := len(f)
|
||||
L := N + M
|
||||
Z := 2*min(N, M) + 2
|
||||
switch {
|
||||
case N > 0 && M > 0:
|
||||
w := N - M
|
||||
g := make([]int, Z)
|
||||
p := make([]int, Z)
|
||||
|
||||
hMax := L/2 + L%2 + 1
|
||||
for h := range hMax {
|
||||
for r := range 2 {
|
||||
var c, d []int
|
||||
var o, m int
|
||||
if r == 0 {
|
||||
c = g
|
||||
d = p
|
||||
o = 1
|
||||
m = 1
|
||||
} else {
|
||||
c = p
|
||||
d = g
|
||||
o = 0
|
||||
m = -1
|
||||
}
|
||||
kMin := -(h - 2*max(0, h-M))
|
||||
kMax := h - 2*max(0, h-N) + 1
|
||||
for k := kMin; k < kMax; k += 2 {
|
||||
var a int
|
||||
if k == -h || k != h && c[pyMod((k-1), Z)] < c[pyMod((k+1), Z)] {
|
||||
a = c[pyMod((k+1), Z)]
|
||||
} else {
|
||||
a = c[pyMod((k-1), Z)] + 1
|
||||
}
|
||||
b := a - k
|
||||
s, t := a, b
|
||||
|
||||
for a < N && b < M && equals(e[(1-o)*N+m*a+(o-1)], f[(1-o)*M+m*b+(o-1)]) {
|
||||
a, b = a+1, b+1
|
||||
}
|
||||
c[pyMod(k, Z)] = a
|
||||
z := -(k - w)
|
||||
if pyMod(L, 2) == o && z >= -(h-o) && z <= h-o && c[pyMod(k, Z)]+d[pyMod(z, Z)] >= N {
|
||||
var D, x, y, u, v int
|
||||
if o == 1 {
|
||||
D = 2*h - 1
|
||||
x = s
|
||||
y = t
|
||||
u = a
|
||||
v = b
|
||||
} else {
|
||||
D = 2 * h
|
||||
x = N - a
|
||||
y = M - b
|
||||
u = N - s
|
||||
v = M - t
|
||||
}
|
||||
switch {
|
||||
case D > 1 || (x != u && y != v):
|
||||
return append(diffInternal(e[0:x], f[0:y], equals, i, j), diffInternal(e[u:N], f[v:M], equals, i+u, j+v)...)
|
||||
case M > N:
|
||||
return diffInternal(make([]T, 0), f[N:M], equals, i+N, j+N)
|
||||
case M < N:
|
||||
return diffInternal(e[M:N], make([]T, 0), equals, i+M, j+M)
|
||||
default:
|
||||
return make([]Op[T], 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case N > 0:
|
||||
res := make([]Op[T], N)
|
||||
for n := range N {
|
||||
res[n] = Op[T]{OpDelete, i + n, -1, e[n]}
|
||||
}
|
||||
return res
|
||||
default:
|
||||
res := make([]Op[T], M)
|
||||
for n := range M {
|
||||
res[n] = Op[T]{OpInsert, i, j + n, f[n]}
|
||||
}
|
||||
return res
|
||||
}
|
||||
panic("Should never hit this!")
|
||||
}
|
||||
|
||||
/**
|
||||
* The remainder op in python always matches the sign of the _denominator_
|
||||
* e.g -1%3 = 2.
|
||||
* In golang it matches the sign of the numerator.
|
||||
* See https://en.wikipedia.org/wiki/Modulo_operation#Variants_of_the_definition
|
||||
*/
|
||||
func pyMod(x, y int) int {
|
||||
return (x%y + y) % y
|
||||
}
|
||||
|
||||
// Let us map element in same way as in
|
||||
|
||||
// Convenient wrapper for string lists
|
||||
func myersDiffStr(e, f []string, compareSpace bool) []Op[string] {
|
||||
return myersDiff[string](e, f, func(s1, s2 string) bool {
|
||||
return compareStrings(s1, s2, compareSpace)
|
||||
})
|
||||
}
|
49
internal/parser/diff/myers_test.go
Normal file
49
internal/parser/diff/myers_test.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package diff
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
t "testing"
|
||||
)
|
||||
|
||||
type TestCase struct {
|
||||
l1 []string
|
||||
l2 []string
|
||||
exp []Op[string]
|
||||
}
|
||||
|
||||
func TestDiff(t *t.T) {
|
||||
A := "A"
|
||||
B := "B"
|
||||
C := "C"
|
||||
testCases := []TestCase{
|
||||
{[]string{}, []string{}, []Op[string]{}},
|
||||
{[]string{}, []string{"foo"}, []Op[string]{{OpInsert, 0, 0, "foo"}}},
|
||||
{[]string{"foo"}, []string{}, []Op[string]{{OpDelete, 0, -1, "foo"}}},
|
||||
{[]string{"foo", "bar", "baz"}, []string{"foo", "bar", "baz"}, []Op[string]{}},
|
||||
{[]string{"foo", "bar", "baz"}, []string{"foo", "baz"}, []Op[string]{{OpDelete, 1, -1, "bar"}}},
|
||||
{[]string{"baz"}, []string{"foo", "baz"}, []Op[string]{{OpInsert, 0, 0, "foo"}}},
|
||||
{[]string{"bar", "baz"}, []string{"foo", "baz"}, []Op[string]{{OpDelete, 0, -1, "bar"}, {OpInsert, 1, 0, "foo"}}},
|
||||
{[]string{"foo", "bar", "baz"}, []string{"foo", "bar"}, []Op[string]{{OpDelete, 2, -1, "baz"}}},
|
||||
{
|
||||
[]string{A, B, C, A, B, B, A},
|
||||
[]string{C, B, A, B, A, C},
|
||||
[]Op[string]{{OpDelete, 0, -1, A}, {OpInsert, 1, 0, C}, {OpDelete, 2, -1, C}, {OpDelete, 5, -1, B}, {OpInsert, 7, 5, C}},
|
||||
},
|
||||
{
|
||||
[]string{C, A, B, A, B, A, B, A, B, A, B, A, B, C},
|
||||
[]string{B, A, B, A, B, A, B, A, B, A, B, A, B, A},
|
||||
[]Op[string]{{OpDelete, 0, -1, C}, {OpInsert, 1, 0, B}, {OpDelete, 13, -1, C}, {OpInsert, 14, 13, A}},
|
||||
},
|
||||
{
|
||||
[]string{B},
|
||||
[]string{A, B, C, B, A},
|
||||
[]Op[string]{{OpInsert, 0, 0, A}, {OpInsert, 0, 1, B}, {OpInsert, 0, 2, C}, {OpInsert, 1, 4, A}},
|
||||
},
|
||||
}
|
||||
for _, c := range testCases {
|
||||
act := myersDiffStr(c.l1, c.l2, true)
|
||||
if !reflect.DeepEqual(c.exp, act) {
|
||||
t.Errorf("Failed diff, expected %v actual %v\n", c.exp, act)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
|
|||
return nil, true, fmt.Errorf("cases number not match")
|
||||
}
|
||||
|
||||
res := make([]stage.ParserResult, 0, len(conf.Cases))
|
||||
var res []stage.ParserResult
|
||||
forceQuit := false
|
||||
for i, caseConf := range conf.Cases {
|
||||
result := results[i]
|
||||
|
@ -71,22 +71,24 @@ func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
|
|||
// Convert answer to string and split by lines
|
||||
answerStr := string(answer)
|
||||
resultStr := result.Files[output.FileName]
|
||||
truncated := false
|
||||
if len(answerStr) > output.MaxDiffLength {
|
||||
answerStr = answerStr[:output.MaxDiffLength]
|
||||
truncated = true
|
||||
}
|
||||
if len(resultStr) > output.MaxDiffLength {
|
||||
resultStr = resultStr[:output.MaxDiffLength]
|
||||
truncated = true
|
||||
}
|
||||
diffOutput := patienceDiff(
|
||||
answerStr, resultStr, output.CompareSpace,
|
||||
answerLines := strings.Split(answerStr, "\n")
|
||||
resultLines := strings.Split(resultStr, "\n")
|
||||
// Generate Myers diff
|
||||
diffOps := myersDiffStr(answerLines, resultLines,
|
||||
output.CompareSpace)
|
||||
// Generate diff block with surrounding context
|
||||
diffOutput := formatDiff(
|
||||
answerLines,
|
||||
resultLines,
|
||||
diffOps,
|
||||
)
|
||||
diffOutput = strings.TrimSuffix(diffOutput, "\n ")
|
||||
if truncated {
|
||||
diffOutput += "\n\n(truncated)"
|
||||
}
|
||||
comment += fmt.Sprintf(
|
||||
"```diff\n%s\n```\n",
|
||||
diffOutput,
|
||||
|
|
|
@ -1,239 +0,0 @@
|
|||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package diff
|
||||
|
||||
// modified from https://github.com/rogpeppe/go-internal/blob/master/diff/diff.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A pair is a pair of values tracked for both the x and y side of a diff.
|
||||
// It is typically a pair of line indexes.
|
||||
type pair struct{ x, y int }
|
||||
|
||||
// Diff returns an anchored diff of the two texts old and new
|
||||
// in the “unified diff” format. If old and new are identical,
|
||||
// Diff returns a nil slice (no output).
|
||||
//
|
||||
// Unix diff implementations typically look for a diff with
|
||||
// the smallest number of lines inserted and removed,
|
||||
// which can in the worst case take time quadratic in the
|
||||
// number of lines in the texts. As a result, many implementations
|
||||
// either can be made to run for a long time or cut off the search
|
||||
// after a predetermined amount of work.
|
||||
//
|
||||
// In contrast, this implementation looks for a diff with the
|
||||
// smallest number of “unique” lines inserted and removed,
|
||||
// where unique means a line that appears just once in both old and new.
|
||||
// We call this an “anchored diff” because the unique lines anchor
|
||||
// the chosen matching regions. An anchored diff is usually clearer
|
||||
// than a standard diff, because the algorithm does not try to
|
||||
// reuse unrelated blank lines or closing braces.
|
||||
// The algorithm also guarantees to run in O(n log n) time
|
||||
// instead of the standard O(n²) time.
|
||||
//
|
||||
// Some systems call this approach a “patience diff,” named for
|
||||
// the “patience sorting” algorithm, itself named for a solitaire card game.
|
||||
// We avoid that name for two reasons. First, the name has been used
|
||||
// for a few different variants of the algorithm, so it is imprecise.
|
||||
// Second, the name is frequently interpreted as meaning that you have
|
||||
// to wait longer (to be patient) for the diff, meaning that it is a slower algorithm,
|
||||
// when in fact the algorithm is faster than the standard one.
|
||||
func patienceDiff(old, new string, compareSpace bool) string {
|
||||
x := strings.SplitAfter(old, "\n")
|
||||
y := strings.SplitAfter(new, "\n")
|
||||
|
||||
// Print diff header.
|
||||
var out bytes.Buffer
|
||||
|
||||
// Loop over matches to consider,
|
||||
// expanding each match to include surrounding lines,
|
||||
// and then printing diff chunks.
|
||||
// To avoid setup/teardown cases outside the loop,
|
||||
// tgs returns a leading {0,0} and trailing {len(x), len(y)} pair
|
||||
// in the sequence of matches.
|
||||
var (
|
||||
done pair // printed up to x[:done.x] and y[:done.y]
|
||||
chunk pair // start lines of current chunk
|
||||
count pair // number of lines from each side in current chunk
|
||||
ctext []string // lines for current chunk
|
||||
)
|
||||
for _, m := range tgs(x, y) {
|
||||
if m.x < done.x {
|
||||
// Already handled scanning forward from earlier match.
|
||||
continue
|
||||
}
|
||||
|
||||
// Expand matching lines as far possible,
|
||||
// establishing that x[start.x:end.x] == y[start.y:end.y].
|
||||
// Note that on the first (or last) iteration we may (or definitely do)
|
||||
// have an empty match: start.x==end.x and start.y==end.y.
|
||||
start := m
|
||||
for start.x > done.x && start.y > done.y && compareStrings(x[start.x-1], y[start.y-1], compareSpace) {
|
||||
start.x--
|
||||
start.y--
|
||||
}
|
||||
end := m
|
||||
for end.x < len(x) && end.y < len(y) && compareStrings(x[end.x], y[end.y], compareSpace) {
|
||||
end.x++
|
||||
end.y++
|
||||
}
|
||||
|
||||
// Emit the mismatched lines before start into this chunk.
|
||||
// (No effect on first sentinel iteration, when start = {0,0}.)
|
||||
for _, s := range x[done.x:start.x] {
|
||||
ctext = append(ctext, "- "+s)
|
||||
count.x++
|
||||
}
|
||||
for _, s := range y[done.y:start.y] {
|
||||
ctext = append(ctext, "+ "+s)
|
||||
count.y++
|
||||
}
|
||||
|
||||
// If we're not at EOF and have too few common lines,
|
||||
// the chunk includes all the common lines and continues.
|
||||
const C = 3 // number of context lines
|
||||
if (end.x < len(x) || end.y < len(y)) &&
|
||||
(end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) {
|
||||
for _, s := range x[start.x:end.x] {
|
||||
ctext = append(ctext, " "+s)
|
||||
count.x++
|
||||
count.y++
|
||||
}
|
||||
done = end
|
||||
continue
|
||||
}
|
||||
|
||||
// End chunk with common lines for context.
|
||||
if len(ctext) > 0 {
|
||||
n := min(end.x-start.x, C)
|
||||
for _, s := range x[start.x : start.x+n] {
|
||||
ctext = append(ctext, " "+s)
|
||||
count.x++
|
||||
count.y++
|
||||
}
|
||||
done = pair{start.x + n, start.y + n}
|
||||
|
||||
// Format and emit chunk.
|
||||
// Convert line numbers to 1-indexed.
|
||||
// Special case: empty file shows up as 0,0 not 1,0.
|
||||
if count.x > 0 {
|
||||
chunk.x++
|
||||
}
|
||||
if count.y > 0 {
|
||||
chunk.y++
|
||||
}
|
||||
// We do not need this line
|
||||
// fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y)
|
||||
for _, s := range ctext {
|
||||
out.WriteString(s)
|
||||
}
|
||||
count.x = 0
|
||||
count.y = 0
|
||||
ctext = ctext[:0]
|
||||
}
|
||||
|
||||
// If we reached EOF, we're done.
|
||||
if end.x >= len(x) && end.y >= len(y) {
|
||||
break
|
||||
}
|
||||
|
||||
// Otherwise start a new chunk.
|
||||
chunk = pair{end.x - C, end.y - C}
|
||||
for _, s := range x[chunk.x:end.x] {
|
||||
ctext = append(ctext, " "+s)
|
||||
count.x++
|
||||
count.y++
|
||||
}
|
||||
done = end
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// tgs returns the pairs of indexes of the longest common subsequence
|
||||
// of unique lines in x and y, where a unique line is one that appears
|
||||
// once in x and once in y.
|
||||
//
|
||||
// The longest common subsequence algorithm is as described in
|
||||
// Thomas G. Szymanski, “A Special Case of the Maximal Common
|
||||
// Subsequence Problem,” Princeton TR #170 (January 1975),
|
||||
// available at https://research.swtch.com/tgs170.pdf.
|
||||
func tgs(x, y []string) []pair {
|
||||
// Count the number of times each string appears in a and b.
|
||||
// We only care about 0, 1, many, counted as 0, -1, -2
|
||||
// for the x side and 0, -4, -8 for the y side.
|
||||
// Using negative numbers now lets us distinguish positive line numbers later.
|
||||
m := make(map[string]int)
|
||||
for _, s := range x {
|
||||
if c := m[s]; c > -2 {
|
||||
m[s] = c - 1
|
||||
}
|
||||
}
|
||||
for _, s := range y {
|
||||
if c := m[s]; c > -8 {
|
||||
m[s] = c - 4
|
||||
}
|
||||
}
|
||||
|
||||
// Now unique strings can be identified by m[s] = -1+-4.
|
||||
//
|
||||
// Gather the indexes of those strings in x and y, building:
|
||||
// xi[i] = increasing indexes of unique strings in x.
|
||||
// yi[i] = increasing indexes of unique strings in y.
|
||||
// inv[i] = index j such that x[xi[i]] = y[yi[j]].
|
||||
var xi, yi, inv []int
|
||||
for i, s := range y {
|
||||
if m[s] == -1+-4 {
|
||||
m[s] = len(yi)
|
||||
yi = append(yi, i)
|
||||
}
|
||||
}
|
||||
for i, s := range x {
|
||||
if j, ok := m[s]; ok && j >= 0 {
|
||||
xi = append(xi, i)
|
||||
inv = append(inv, j)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply Algorithm A from Szymanski's paper.
|
||||
// In those terms, A = J = inv and B = [0, n).
|
||||
// We add sentinel pairs {0,0}, and {len(x),len(y)}
|
||||
// to the returned sequence, to help the processing loop.
|
||||
J := inv
|
||||
n := len(xi)
|
||||
T := make([]int, n)
|
||||
L := make([]int, n)
|
||||
for i := range T {
|
||||
T[i] = n + 1
|
||||
}
|
||||
for i := range n {
|
||||
k := sort.Search(n, func(k int) bool {
|
||||
return T[k] >= J[i]
|
||||
})
|
||||
T[k] = J[i]
|
||||
L[i] = k + 1
|
||||
}
|
||||
k := 0
|
||||
for _, v := range L {
|
||||
if k < v {
|
||||
k = v
|
||||
}
|
||||
}
|
||||
seq := make([]pair, 2+k)
|
||||
seq[1+k] = pair{len(x), len(y)} // sentinel at end
|
||||
lastj := n
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
if L[i] == k && J[i] < lastj {
|
||||
seq[k] = pair{xi[i], yi[J[i]]}
|
||||
k--
|
||||
}
|
||||
}
|
||||
seq[0] = pair{0, 0} // sentinel at start
|
||||
return seq
|
||||
}
|
|
@ -11,7 +11,7 @@ func (*Dummy) Run(results []stage.ExecutorResult, confAny any) (
|
|||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
res := make([]stage.ParserResult, 0, len(results))
|
||||
var res []stage.ParserResult
|
||||
for range results {
|
||||
res = append(res, stage.ParserResult{Score: conf.Score, Comment: conf.Comment})
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ func (p *Healthcheck) Run(results []stage.ExecutorResult, confAny any) (
|
|||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
res := make([]stage.ParserResult, 0, len(results))
|
||||
var res []stage.ParserResult
|
||||
forceQuit := false
|
||||
for _, result := range results {
|
||||
parserResult, forceQuitResult := p.parse(result, *conf)
|
||||
|
|
|
@ -32,7 +32,7 @@ func (*Keyword) parse(executorResult stage.ExecutorResult, conf Conf) stage.Pars
|
|||
Count int
|
||||
ScoreChange int
|
||||
}
|
||||
results := make([]Result, 0, len(matchCount))
|
||||
var results []Result
|
||||
for keyword, count := range matchCount {
|
||||
results = append(results, Result{
|
||||
Keyword: keyword,
|
||||
|
@ -66,7 +66,7 @@ func (p *Keyword) Run(results []stage.ExecutorResult, confAny any) (
|
|||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
res := make([]stage.ParserResult, 0, len(results))
|
||||
var res []stage.ParserResult
|
||||
forceQuit := false
|
||||
for _, result := range results {
|
||||
parseRes := p.parse(result, *conf)
|
||||
|
|
|
@ -43,7 +43,7 @@ func (p *Log) Run(results []stage.ExecutorResult, confAny any) (
|
|||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
res := make([]stage.ParserResult, 0, len(results))
|
||||
var res []stage.ParserResult
|
||||
for _, result := range results {
|
||||
res = append(res, p.parse(result, *conf))
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ func (*ResultDetail) Run(results []stage.ExecutorResult, confAny any) (
|
|||
return nil, true, err
|
||||
}
|
||||
forceQuit := false
|
||||
res := make([]stage.ParserResult, 0, len(results))
|
||||
var res []stage.ParserResult
|
||||
for _, result := range results {
|
||||
comment := ""
|
||||
if conf.ShowExecutorStatus {
|
||||
|
|
|
@ -15,7 +15,7 @@ func (*ResultStatus) Run(results []stage.ExecutorResult, confAny any) (
|
|||
}
|
||||
score := conf.Score
|
||||
forceQuit := false
|
||||
res := make([]stage.ParserResult, 0, len(results))
|
||||
var res []stage.ParserResult
|
||||
for _, result := range results {
|
||||
comment := conf.Comment
|
||||
if result.Status != stage.StatusAccepted {
|
||||
|
|
|
@ -32,7 +32,7 @@ func (p *Sample) Run(results []stage.ExecutorResult, confAny any) (
|
|||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
res := make([]stage.ParserResult, 0, len(results))
|
||||
var res []stage.ParserResult
|
||||
for _, result := range results {
|
||||
res = append(res, p.parse(result, *conf))
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ func (*TierScore) Run(results []stage.ExecutorResult, confAny any) (
|
|||
return nil, true, err
|
||||
}
|
||||
|
||||
res := make([]stage.ParserResult, 0, len(results))
|
||||
var res []stage.ParserResult
|
||||
forceQuit := false
|
||||
|
||||
for _, result := range results {
|
||||
|
|
|
@ -36,7 +36,7 @@ func Run(stages []Stage) (
|
|||
"name", stage.Executor.Name,
|
||||
)
|
||||
err = fmt.Errorf("executor not found: %s", stage.Executor.Name)
|
||||
return stageResults, forceQuitStageName, err
|
||||
return
|
||||
}
|
||||
executorResults, err = executor.Run(stage.Executor.Cmds)
|
||||
if err != nil {
|
||||
|
@ -46,7 +46,7 @@ func Run(stages []Stage) (
|
|||
"name", stage.Executor.Name,
|
||||
"error", err,
|
||||
)
|
||||
return stageResults, forceQuitStageName, err
|
||||
return
|
||||
}
|
||||
for i, executorResult := range executorResults {
|
||||
slog.Debug(
|
||||
|
@ -92,7 +92,7 @@ func Run(stages []Stage) (
|
|||
"name", stageParser.Name,
|
||||
)
|
||||
err = fmt.Errorf("parser not found: %s", stageParser.Name)
|
||||
return stageResults, forceQuitStageName, err
|
||||
return
|
||||
}
|
||||
var parserForceQuit bool
|
||||
tmpParserResults, parserForceQuit, err = parser.Run(
|
||||
|
@ -154,7 +154,7 @@ func Run(stages []Stage) (
|
|||
break
|
||||
}
|
||||
}
|
||||
return stageResults, forceQuitStageName, err
|
||||
return
|
||||
}
|
||||
|
||||
func Cleanup() {
|
||||
|
|
|
@ -76,7 +76,7 @@ func StringToStatus(s string) (Status, error) {
|
|||
|
||||
// MarshalJSON convert status into string
|
||||
func (s Status) MarshalJSON() ([]byte, error) {
|
||||
return []byte("\"" + s.String() + "\""), nil
|
||||
return []byte("\"" + Status(s).String() + "\""), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON convert string into status
|
||||
|
@ -86,7 +86,7 @@ func (s *Status) UnmarshalJSON(b []byte) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*s = v
|
||||
*s = Status(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -31,12 +31,12 @@ func All(
|
|||
res.Msg += fmt.Sprintf("### Meta File Check Failed:\n%s\n", err.Error())
|
||||
res.Failed = true
|
||||
}
|
||||
err = NonASCIIFiles(rootDir)
|
||||
err = NonAsciiFiles(rootDir)
|
||||
if err != nil {
|
||||
res.Msg += fmt.Sprintf("### Non-ASCII Characters File Check Failed:\n%s\n", err.Error())
|
||||
res.Failed = true
|
||||
}
|
||||
err = NonASCIIMsg(rootDir)
|
||||
err = NonAsciiMsg(rootDir)
|
||||
if err != nil {
|
||||
res.Msg += fmt.Sprintf("### Non-ASCII Characters Commit Message Check Failed:\n%s\n", err.Error())
|
||||
res.Failed = true
|
||||
|
@ -46,5 +46,5 @@ func All(
|
|||
res.Msg += fmt.Sprintf("### Repo File Check Failed:\n%s\n", err.Error())
|
||||
res.Failed = true
|
||||
}
|
||||
return res
|
||||
return
|
||||
}
|
||||
|
|
|
@ -44,12 +44,12 @@ func checkMsg(msg string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// NonASCIIMsg checks for non-ASCII characters in the commit message.
|
||||
// nonAsciiMsg checks for non-ASCII characters in the commit message.
|
||||
// It iterates over each character in the message and checks if it is a non-ASCII character.
|
||||
// If a non-ASCII character is found, it returns an error indicating not to use non-ASCII characters in commit messages.
|
||||
// Otherwise, it returns nil indicating that the commit message is valid.
|
||||
// It skips the non-ASCII characters check for lines starting with specific keywords like "Co-authored-by", "Reviewed-by", and "Co-committed-by".
|
||||
func NonASCIIMsg(root string) error {
|
||||
func NonAsciiMsg(root string) error {
|
||||
repo, err := git.PlainOpen(root)
|
||||
if err != nil {
|
||||
slog.Error("opening git repo", "err", err)
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
// getMetas retrieves a list of metadata files that are expected to exist in the specified root directory.
|
||||
// It checks for the existence of each file in the fileList and provides instructions if any file is missing.
|
||||
func getMetas(rootDir string, fileList []string) ([]string, string, error) {
|
||||
regexList := make([]*regexp.Regexp, 0, len(fileList))
|
||||
var regexList []*regexp.Regexp
|
||||
for _, file := range fileList {
|
||||
pattern := "(?i)" + file
|
||||
if !strings.Contains(pattern, "\\.") {
|
||||
|
|
|
@ -12,10 +12,10 @@ import (
|
|||
"github.com/go-git/go-git/v5/plumbing/format/gitattributes"
|
||||
)
|
||||
|
||||
// getNonASCII retrieves a list of files in the specified root directory that contain non-ASCII characters.
|
||||
// getNonAscii retrieves a list of files in the specified root directory that contain non-ASCII characters.
|
||||
// It searches for non-ASCII characters in each file's content and returns a list of paths to files containing non-ASCII characters.
|
||||
func getNonASCII(root string) ([]string, error) {
|
||||
var nonASCII []string
|
||||
func getNonAscii(root string) ([]string, error) {
|
||||
var nonAscii []string
|
||||
gitattrExist := true
|
||||
var matcher gitattributes.Matcher
|
||||
_, err := os.Stat(".gitattributes")
|
||||
|
@ -71,7 +71,7 @@ func getNonASCII(root string) ([]string, error) {
|
|||
cont := true
|
||||
for _, c := range scanner.Text() {
|
||||
if c > unicode.MaxASCII {
|
||||
nonASCII = append(nonASCII, "\t"+path)
|
||||
nonAscii = append(nonAscii, "\t"+path)
|
||||
cont = false
|
||||
break
|
||||
}
|
||||
|
@ -84,20 +84,20 @@ func getNonASCII(root string) ([]string, error) {
|
|||
return nil
|
||||
})
|
||||
|
||||
return nonASCII, err
|
||||
return nonAscii, err
|
||||
}
|
||||
|
||||
// NonASCIIFiles checks for non-ASCII characters in files within the specified root directory.
|
||||
// nonAsciiFiles checks for non-ASCII characters in files within the specified root directory.
|
||||
// It prints a message with the paths to files containing non-ASCII characters, if any.
|
||||
func NonASCIIFiles(root string) error {
|
||||
nonASCII, err := getNonASCII(root)
|
||||
func NonAsciiFiles(root string) error {
|
||||
nonAscii, err := getNonAscii(root)
|
||||
if err != nil {
|
||||
slog.Error("getting non-ascii", "err", err)
|
||||
return fmt.Errorf("error getting non-ascii: %w", err)
|
||||
}
|
||||
if len(nonASCII) > 0 {
|
||||
if len(nonAscii) > 0 {
|
||||
return fmt.Errorf("Non-ASCII characters found in the following files:\n%s",
|
||||
strings.Join(nonASCII, "\n"))
|
||||
strings.Join(nonAscii, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user