diff --git a/pkg/ast/workflow.go b/pkg/ast/workflow.go index 3b2e410e..40929380 100644 --- a/pkg/ast/workflow.go +++ b/pkg/ast/workflow.go @@ -27,7 +27,7 @@ type JobRef struct { // not the job that will be executed StepName string StepNameRange protocol.Range - Requires []TextAndRange + Requires []Require Context []TextAndRange Type string TypeRange protocol.Range @@ -44,8 +44,9 @@ type JobRef struct { } type Require struct { - Name string - Range protocol.Range + Name string + Status []string + Range protocol.Range } type WorkflowTrigger struct { diff --git a/pkg/parser/validate/workflow.go b/pkg/parser/validate/workflow.go index 82589797..30e2291e 100644 --- a/pkg/parser/validate/workflow.go +++ b/pkg/parser/validate/workflow.go @@ -42,10 +42,10 @@ func (val Validate) validateSingleWorkflow(workflow ast.Workflow) error { val.validateWorkflowParameters(jobRef, jobRef.JobName, jobRef.JobRefRange) } for _, require := range jobRef.Requires { - if !val.doesJobRefExist(workflow, require.Text) && !utils.CheckIfMatrixParamIsPartiallyReferenced(require.Text) { + if !val.doesJobRefExist(workflow, require.Name) && !utils.CheckIfMatrixParamIsPartiallyReferenced(require.Name) { val.addDiagnostic(utils.CreateErrorDiagnosticFromRange( require.Range, - fmt.Sprintf("Cannot find declaration for job reference %s", require.Text))) + fmt.Sprintf("Cannot find declaration for job reference %s", require.Name))) } } diff --git a/pkg/parser/workflows.go b/pkg/parser/workflows.go index 8c30aa65..db92022d 100644 --- a/pkg/parser/workflows.go +++ b/pkg/parser/workflows.go @@ -102,7 +102,7 @@ func (doc *YamlDocument) buildJobsDAG(jobRefs []ast.JobRef) map[string][]string res := make(map[string][]string) for _, jobRef := range jobRefs { for _, requirement := range jobRef.Requires { - res[requirement.Text] = append(res[requirement.Text], jobRef.StepName) + res[requirement.Name] = append(res[requirement.Name], jobRef.StepName) } } return res @@ -246,12 +246,70 @@ func (doc *YamlDocument) parseContext(node *sitter.Node) []ast.TextAndRange { return doc.getNodeTextArrayWithRange(node) } -func (doc *YamlDocument) parseSingleJobRequires(node *sitter.Node) []ast.TextAndRange { - array := doc.getNodeTextArrayWithRange(node) - res := []ast.TextAndRange{} - for _, require := range array { - res = append(res, ast.TextAndRange{Text: require.Text, Range: require.Range}) +func (doc *YamlDocument) parseSingleJobRequires(requiresNode *sitter.Node) []ast.Require { + blockSequenceNode := GetChildSequence(requiresNode) + res := make([]ast.Require, 0, requiresNode.ChildCount()) + + if blockSequenceNode == nil { + return res } + + iterateOnBlockSequence(blockSequenceNode, func(requiresItemNode *sitter.Node) { + getRequire := func(node *sitter.Node) ast.Require { + defaultStatus := []string{"success"} + if alias := GetChildOfType(node, "alias"); alias != nil { + anchor, ok := doc.YamlAnchors[strings.TrimLeft(doc.GetNodeText(alias), "*")] + if !ok { + return ast.Require{Name: ""} + } + anchorValueNode := GetFirstChild(anchor.ValueNode) + text := doc.GetNodeText(anchorValueNode) + return ast.Require{Name: text, Status: defaultStatus, Range: doc.NodeToRange(anchorValueNode)} + } else { + return ast.Require{Name: doc.GetNodeText(node), Status: defaultStatus, Range: doc.NodeToRange(node)} + } + } + + // If blockSequenceNode is a flow_sequence, then requiresItemNode is directly a flow_node + if requiresItemNode.Type() == "flow_node" { + res = append(res, getRequire(requiresItemNode)) + } else { + // But if blockSequenceNode is a block_sequence, then requiresItemNode is a block_sequence_item + // The first child of requiresItemNode is the hyphen node, the second child is what we need + element := requiresItemNode.Child(1) + // If the second child is a flow_node, then it is a simple require + if element != nil && element.Type() == "flow_node" { + res = append(res, getRequire(element)) + } else { + // Otherwise the second child is a block_mapping, then it is a require with status + blockMappingNode := GetChildOfType(element, "block_mapping") + blockMappingPair := GetChildOfType(blockMappingNode, "block_mapping_pair") + key, value := doc.GetKeyValueNodes(blockMappingPair) + + if key == nil || value == nil { + return + } + if GetFirstChild(value).Type() == "plain_scalar" { + status := make([]string, 1) + status[0] = doc.GetNodeText(value) + res = append(res, ast.Require{Name: doc.GetNodeText(key), Status: status, Range: doc.NodeToRange(key)}) + } else { + statusesNode := GetFirstChild(value) + status := make([]string, 0, statusesNode.ChildCount()) + iterateOnBlockSequence(statusesNode, func(statusItemNode *sitter.Node) { + if statusItemNode.Type() == "flow_node" { + status = append(status, doc.GetNodeText(statusItemNode)) + } + if statusItemNode.Type() == "block_sequence_item" { + status = append(status, doc.GetNodeText(statusItemNode.Child(1))) + } + }) + res = append(res, ast.Require{Name: doc.GetNodeText(key), Status: status, Range: doc.NodeToRange(key)}) + } + } + } + }) + return res } diff --git a/pkg/parser/workflows_test.go b/pkg/parser/workflows_test.go index 02a2e0f5..cc1364f0 100644 --- a/pkg/parser/workflows_test.go +++ b/pkg/parser/workflows_test.go @@ -35,6 +35,14 @@ func TestYamlDocument_parseSingleJobReference(t *testing.T) { const jobRef4 = ` - test: name: say-my-name` + const jobRef5 = ` +- test: + requires: + - setup: failed` + const jobRef6 = ` +- test: + requires: + - setup: [success, canceled]` type fields struct { Content []byte @@ -128,9 +136,10 @@ func TestYamlDocument_parseSingleJobReference(t *testing.T) { Character: 6, }, }, - Requires: []ast.TextAndRange{ + Requires: []ast.Require{ { - Text: "setup", + Name: "setup", + Status: []string{"success"}, Range: protocol.Range{ Start: protocol.Position{Line: 3, Character: 10}, End: protocol.Position{Line: 3, Character: 15}, @@ -304,6 +313,108 @@ func TestYamlDocument_parseSingleJobReference(t *testing.T) { MatrixParams: make(map[string][]ast.ParameterValue), }, }, + { + name: "Job reference with requires and single status", + fields: fields{Content: []byte(jobRef5)}, + args: args{jobRefNode: getFirstChildOfType(GetRootNode([]byte(jobRef5)), "block_sequence_item")}, + want: ast.JobRef{ + JobName: "test", + JobRefRange: protocol.Range{ + Start: protocol.Position{ + Line: 1, + Character: 0, + }, + End: protocol.Position{ + Line: 3, + Character: 23, + }, + }, + JobNameRange: protocol.Range{ + Start: protocol.Position{ + Line: 1, + Character: 2, + }, + End: protocol.Position{ + Line: 1, + Character: 6, + }, + }, + StepName: "test", + StepNameRange: protocol.Range{ + Start: protocol.Position{ + Line: 1, + Character: 2, + }, + End: protocol.Position{ + Line: 1, + Character: 6, + }, + }, + Requires: []ast.Require{ + { + Name: "setup", + Status: []string{"failed"}, + Range: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 10}, + End: protocol.Position{Line: 3, Character: 15}, + }, + }, + }, + MatrixParams: make(map[string][]ast.ParameterValue), + Parameters: make(map[string]ast.ParameterValue), + }, + }, + { + name: "Job reference with requires and multiple statuses", + fields: fields{Content: []byte(jobRef6)}, + args: args{jobRefNode: getFirstChildOfType(GetRootNode([]byte(jobRef6)), "block_sequence_item")}, + want: ast.JobRef{ + JobName: "test", + JobRefRange: protocol.Range{ + Start: protocol.Position{ + Line: 1, + Character: 0, + }, + End: protocol.Position{ + Line: 3, + Character: 36, + }, + }, + JobNameRange: protocol.Range{ + Start: protocol.Position{ + Line: 1, + Character: 2, + }, + End: protocol.Position{ + Line: 1, + Character: 6, + }, + }, + StepName: "test", + StepNameRange: protocol.Range{ + Start: protocol.Position{ + Line: 1, + Character: 2, + }, + End: protocol.Position{ + Line: 1, + Character: 6, + }, + }, + Requires: []ast.Require{ + { + Name: "setup", + Status: []string{"success", "canceled"}, + Range: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 10}, + End: protocol.Position{Line: 3, Character: 15}, + }, + }, + }, + MatrixParams: make(map[string][]ast.ParameterValue), + Parameters: make(map[string]ast.ParameterValue), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/services/definition/workflows.go b/pkg/services/definition/workflows.go index 838b3def..28218eea 100644 --- a/pkg/services/definition/workflows.go +++ b/pkg/services/definition/workflows.go @@ -29,11 +29,11 @@ func (def DefinitionStruct) searchForWorkflows() []protocol.Location { return []protocol.Location{} } -func (def DefinitionStruct) searchForWorkflowJobsRequires(requires []ast.TextAndRange, workflow ast.Workflow) []protocol.Location { +func (def DefinitionStruct) searchForWorkflowJobsRequires(requires []ast.Require, workflow ast.Workflow) []protocol.Location { for _, require := range requires { if utils.PosInRange(require.Range, def.Params.Position) { for _, jobRef := range workflow.JobRefs { - if jobRef.JobName == require.Text { + if jobRef.JobName == require.Name { return []protocol.Location{ { URI: def.Params.TextDocument.URI, diff --git a/pkg/services/diagnostics_test.go b/pkg/services/diagnostics_test.go index 16f94305..968f3899 100644 --- a/pkg/services/diagnostics_test.go +++ b/pkg/services/diagnostics_test.go @@ -36,6 +36,11 @@ func TestFindErrors(t *testing.T) { args: args{filePath: "./testdata/anchorNoErrors.yml"}, want: make([]protocol.Diagnostic, 0), }, + { + name: "No errors", + args: args{filePath: "./testdata/requiresNoErrors.yml"}, + want: make([]protocol.Diagnostic, 0), + }, } for _, tt := range tests { diff --git a/pkg/services/testdata/requiresNoErrors.yml b/pkg/services/testdata/requiresNoErrors.yml new file mode 100644 index 00000000..c8f67866 --- /dev/null +++ b/pkg/services/testdata/requiresNoErrors.yml @@ -0,0 +1,41 @@ +version: 2.1 + +jobs: + build: + docker: + - image: node:latest + steps: + - checkout + - run: echo "build" + somejob: + docker: + - image: node:latest + steps: + - checkout + - run: echo "somejob" + someotherjob: + docker: + - image: node:latest + steps: + - checkout + - run: echo "somejob" + anotherjob: + docker: + - image: node:latest + steps: + - checkout + - run: echo "anotherjob" + +workflows: + test-build: + jobs: + - build + - somejob + - someotherjob + - anotherjob: + requires: + - build: failed + - somejob: + - success + - canceled + - someotherjob: [canceled, failed] diff --git a/schema.json b/schema.json index ca3b0a82..d954614f 100644 --- a/schema.json +++ b/schema.json @@ -1525,7 +1525,33 @@ "requires": { "type": "array", "items": { - "type": "string" + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "minProperties": 1, + "maxProperties": 1, + "patternProperties": { + "^[A-Za-z][A-Za-z\\s\\d_-]*$": { + "oneOf": [ + { + "type": "string", + "enum": ["success", "failed", "canceled"] + }, + { + "type": "array", + "minLength": 1, + "items": { + "type": "string", + "enum": ["success", "failed", "canceled"] + } + } + ] + }} + } + ] } }, "filters": {