diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 631ebb9..73cb03a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,6 +4,9 @@ on: push: branches: - main + pull_request: + branches: + - main jobs: diff --git a/pkg/ast/ast.go b/pkg/ast/ast.go index 09ae1d5..af376bd 100644 --- a/pkg/ast/ast.go +++ b/pkg/ast/ast.go @@ -4,6 +4,7 @@ package ast import ( "fmt" "math/rand" + "slices" "strings" ) @@ -105,12 +106,20 @@ func (ai *AddInstructionNode) Instruction() string { return "ADD" } // ARG type ArgInstructionNode struct { - Name string - Value string // optional default + Pairs map[string]string } func (ai *ArgInstructionNode) ToString() string { - return fmt.Sprintf("%sARG%s %s %s %s", colorPurple, colorCyan, ai.Name, ai.Value, colorNone) + mapStrings := []string{} + keys := make([]string, 0, len(ai.Pairs)) + for k := range ai.Pairs { + keys = append(keys, k) + } + slices.Sort(keys) + for _, k := range keys { + mapStrings = append(mapStrings, fmt.Sprintf("%s=%s", k, ai.Pairs[k])) + } + return fmt.Sprintf("%sARG%s %s %s", colorPurple, colorCyan, strings.Join(mapStrings, ","), colorNone) } func (ai *ArgInstructionNode) Instruction() string { return "ARG" } diff --git a/pkg/ast/reconstruct.go b/pkg/ast/reconstruct.go index 45c040d..109a4a0 100644 --- a/pkg/ast/reconstruct.go +++ b/pkg/ast/reconstruct.go @@ -26,11 +26,12 @@ func escapeSlice[T any](slice []T) string { func (sn *StageNode) Reconstruct() []string { reconstructed := []string{} if sn.Image != "" { - fromInstruction := fmt.Sprintf("FROM %s", sn.Image) + var fromInstruction strings.Builder + fromInstruction.WriteString(fmt.Sprintf("FROM %s", sn.Image)) if sn.Name != "" { - fromInstruction += fmt.Sprintf(" AS %s", sn.Name) + fromInstruction.WriteString(fmt.Sprintf(" AS %s", sn.Name)) } - reconstructed = append(reconstructed, fromInstruction) + reconstructed = append(reconstructed, fromInstruction.String()) } for _, instructionNode := range sn.Instructions { reconstructed = append(reconstructed, instructionNode.Reconstruct()...) @@ -55,7 +56,17 @@ func (ai *AddInstructionNode) Reconstruct() []string { } func (ai *ArgInstructionNode) Reconstruct() []string { - reconstructed := fmt.Sprintf("%s %s=%s", ai.Instruction(), ai.Name, ai.Value) + reconstructed := fmt.Sprintf("%s", ai.Instruction()) + keys := make([]string, len(ai.Pairs)) + index := 0 + for k := range ai.Pairs { + keys[index] = k + index++ + } + slices.Sort(keys) + for _, k := range keys { + reconstructed += fmt.Sprintf(" %s=%s", k, ai.Pairs[k]) + } return []string{reconstructed} } @@ -64,22 +75,24 @@ func (ci *CmdInstructionNode) Reconstruct() []string { return []string{reconstructed} } func (ci *CopyInstructionNode) Reconstruct() []string { - reconstructed := fmt.Sprintf("%s ", ci.Instruction()) + var reconstructed strings.Builder + reconstructed.WriteString(fmt.Sprintf("%s ", ci.Instruction())) - reconstructed += formatIfValue("--keep-git-dir=%s ", strconv.FormatBool(ci.KeepGitDir)) - reconstructed += formatIfValue("--chown=%s ", ci.Chown) - reconstructed += formatIfValue("--link=%s ", strconv.FormatBool(ci.Link)) - reconstructed += formatIfValue("--from=%s ", ci.From) - reconstructed += fmt.Sprintf("%s ", strings.Join(ci.Source, " ")) - reconstructed += fmt.Sprintf("%s", ci.Destination) - return []string{reconstructed} + reconstructed.WriteString(formatIfValue("--keep-git-dir=%s ", strconv.FormatBool(ci.KeepGitDir))) + reconstructed.WriteString(formatIfValue("--chown=%s ", ci.Chown)) + reconstructed.WriteString(formatIfValue("--link=%s ", strconv.FormatBool(ci.Link))) + reconstructed.WriteString(formatIfValue("--from=%s ", ci.From)) + reconstructed.WriteString(fmt.Sprintf("%s ", strings.Join(ci.Source, " "))) + reconstructed.WriteString(fmt.Sprintf("%s", ci.Destination)) + return []string{reconstructed.String()} } func (ei *EntrypointInstructionNode) Reconstruct() []string { reconstructed := fmt.Sprintf("%s %s", ei.Instruction(), escapeSlice(ei.Exec)) return []string{reconstructed} } func (ei *EnvInstructionNode) Reconstruct() []string { - reconstructed := fmt.Sprintf("%s", ei.Instruction()) + var reconstructed strings.Builder + reconstructed.WriteString(fmt.Sprintf("%s", ei.Instruction())) keys := make([]string, len(ei.Pairs)) index := 0 for k := range ei.Pairs { @@ -88,41 +101,52 @@ func (ei *EnvInstructionNode) Reconstruct() []string { } slices.Sort(keys) for _, k := range keys { - reconstructed += fmt.Sprintf(" %s=%s", k, ei.Pairs[k]) + reconstructed.WriteString(fmt.Sprintf(" %s=%s", k, ei.Pairs[k])) } - return []string{reconstructed} + return []string{reconstructed.String()} } func (ei *ExposeInstructionNode) Reconstruct() []string { - reconstructed := fmt.Sprintf("%s", ei.Instruction()) + var reconstructed strings.Builder + reconstructed.WriteString(fmt.Sprintf("%s", ei.Instruction())) for _, port := range ei.Ports { protocol := "tcp" if !port.IsTCP { protocol = "udp" } - reconstructed += fmt.Sprintf(" %s/%s", port.Port, protocol) + reconstructed.WriteString(fmt.Sprintf(" %s/%s", port.Port, protocol)) } - return []string{reconstructed} + return []string{reconstructed.String()} } func (hi *HealthcheckInstructionNode) Reconstruct() []string { - reconstructed := fmt.Sprintf("%s ", hi.Instruction()) + var reconstructed strings.Builder + reconstructed.WriteString(fmt.Sprintf("%s ", hi.Instruction())) if hi.CancelStatement { - return []string{reconstructed + "NONE"} + reconstructed.WriteString("NONE") + return []string{reconstructed.String()} } - reconstructed += formatIfValue("--interval=%s ", hi.Interval) - reconstructed += formatIfValue("--timeout=%s ", hi.Timeout) - reconstructed += formatIfValue("--start-period=%s ", hi.StartPeriod) - reconstructed += formatIfValue("--start-interval=%s ", hi.StartInterval) - reconstructed += formatIfValue("--retries=%s ", strconv.Itoa(hi.Retries)) - reconstructed += escapeSlice(hi.Cmd) - return []string{reconstructed} + reconstructed.WriteString(formatIfValue("--interval=%s ", hi.Interval)) + reconstructed.WriteString(formatIfValue("--timeout=%s ", hi.Timeout)) + reconstructed.WriteString(formatIfValue("--start-period=%s ", hi.StartPeriod)) + reconstructed.WriteString(formatIfValue("--start-interval=%s ", hi.StartInterval)) + reconstructed.WriteString(formatIfValue("--retries=%s ", strconv.Itoa(hi.Retries))) + reconstructed.WriteString(escapeSlice(hi.Cmd)) + return []string{reconstructed.String()} } func (li *LabelInstructionNode) Reconstruct() []string { - reconstructed := fmt.Sprintf("%s", li.Instruction()) - for k, v := range li.Pairs { - reconstructed += fmt.Sprintf(" %s=%s", k, v) + var reconstructed strings.Builder + reconstructed.WriteString(fmt.Sprintf("%s", li.Instruction())) + keys := make([]string, len(li.Pairs)) + index := 0 + for k := range li.Pairs { + keys[index] = k + index++ } - return []string{reconstructed} + slices.Sort(keys) + for _, k := range keys { + reconstructed.WriteString(fmt.Sprintf(" %s=%s", k, li.Pairs[k])) + } + return []string{reconstructed.String()} } func (mi *MaintainerInstructionNode) Reconstruct() []string { reconstructed := fmt.Sprintf("%s %s", mi.Instruction(), mi.Name) @@ -135,14 +159,16 @@ func (oi *OnbuildInstructionNode) Reconstruct() []string { return nested } func (ri *RunInstructionNode) Reconstruct() []string { - reconstructed := fmt.Sprintf("%s ", ri.Instruction()) + var reconstructed strings.Builder + reconstructed.WriteString(fmt.Sprintf("%s ", ri.Instruction())) if !ri.ShellForm && !ri.IsHeredoc { - return []string{reconstructed + escapeSlice(ri.Cmd)} + reconstructed.WriteString(escapeSlice(ri.Cmd)) + return []string{reconstructed.String()} } if ri.IsHeredoc { - reconstructed += "<< " + reconstructed.WriteString("<< ") } - ri.Cmd[0] = reconstructed + ri.Cmd[0] + ri.Cmd[0] = reconstructed.String() + ri.Cmd[0] return ri.Cmd } func (si *ShellInstructionNode) Reconstruct() []string { diff --git a/pkg/ast/reconstruct_test.go b/pkg/ast/reconstruct_test.go index 120645c..1657fb6 100644 --- a/pkg/ast/reconstruct_test.go +++ b/pkg/ast/reconstruct_test.go @@ -58,13 +58,28 @@ func TestReconstructInstruction(t *testing.T) { Input: ast.StageNode{ Instructions: []ast.InstructionNode{ &ast.ArgInstructionNode{ - Name: "abc", - Value: "def", + Pairs: map[string]string{ + "abc": "def", + }, }, }, }, Expected: []string{"ARG abc=def"}, }, + { + Input: ast.StageNode{ + Instructions: []ast.InstructionNode{ + &ast.ArgInstructionNode{ + Pairs: map[string]string{ + "abc": "def", + "xy": "z", + }, + }, + }, + }, + Expected: []string{"ARG abc=def xy=z"}, + }, + { Input: ast.StageNode{ Instructions: []ast.InstructionNode{ diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index 298e3fa..3127868 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -155,13 +155,12 @@ func (p Parser) parseAdd(t token.Token) ast.InstructionNode { } func (p Parser) parseArg(t token.Token) ast.InstructionNode { - key, value := util.ParseAssign(t.Content) - if len(key)+len(value) == 0 { - key = t.Content + pairs := util.ParseAssigns(t.Content) + if len(pairs) == 0 { + pairs[t.Content] = "" } return &ast.ArgInstructionNode{ - Name: key, - Value: value, + Pairs: pairs, } } diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index df5c13d..5a97ff6 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -116,7 +116,7 @@ func compareInstructionNode(expected, actual ast.InstructionNode) string { case *ast.AddInstructionNode: return compareAddInstructionNode(expected.(*ast.AddInstructionNode), ac) case *ast.ArgInstructionNode: - if *expected.(*ast.ArgInstructionNode) != *ac { + if !reflect.DeepEqual(expected.(*ast.ArgInstructionNode).Pairs, ac.Pairs) { return fmt.Sprintf("ARG instruction mismatch: Expected %v Got %v", expected, ac) } case *ast.CmdInstructionNode: @@ -314,8 +314,7 @@ func TestInstructionParsing(t *testing.T) { }, }, Expected: []ast.InstructionNode{&ast.ArgInstructionNode{ - Name: "test", - Value: "value", + Pairs: map[string]string{"test": "value"}, }}, }, { @@ -326,8 +325,7 @@ func TestInstructionParsing(t *testing.T) { }, }, Expected: []ast.InstructionNode{&ast.ArgInstructionNode{ - Name: "test", - Value: "", + Pairs: map[string]string{"test": ""}, }}, }, {