You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ci_validator/gitlabcivalidator/parser.go

185 lines
5.5 KiB
Go

package gitlabcivalidator
import (
"fmt"
"gopkg.in/yaml.v3"
"tea.gitpark.ru/Azaki/CI_VALIDATOR/errors"
)
// ParseGitLabCIConfig parses the YAML and returns the GitLabCIConfig structure.
func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConfig, error) {
var config GitLabCIConfig
var root yaml.Node
// Parse YAML into the root node.
err := yaml.Unmarshal(data, &root)
if err != nil {
return nil, fmt.Errorf("YAML syntax error: %w", err)
}
if len(root.Content) == 0 {
return nil, fmt.Errorf(".gitlab-ci.yml file is empty or does not contain valid data")
}
if root.Kind != yaml.DocumentNode || len(root.Content) == 0 {
return nil, fmt.Errorf("invalid YAML format: expected a root object")
}
mainNode := root.Content[0]
// Decode known top-level keys into config.
err = mainNode.Decode(&config)
if err != nil {
return nil, fmt.Errorf("failed to decode GitLab CI configuration: %w", err)
}
// Allowed top-level keys that are NOT jobs.
allowedKeys := map[string]struct{}{
"default": {},
"include": {},
"before_script": {},
"image": {},
"services": {},
"after_script": {},
"variables": {},
"stages": {},
"cache": {},
"workflow": {},
"changes": {},
"paths": {},
}
// Initialize config.Jobs if not already set.
if config.Jobs == nil {
config.Jobs = make(map[string]*Job)
}
// Now, iterate over the top-level mapping keys.
if mainNode.Kind == yaml.MappingNode {
for i := 0; i < len(mainNode.Content)-1; i += 2 {
keyNode := mainNode.Content[i]
valNode := mainNode.Content[i+1]
if keyNode.Value == "variables" {
config.VariablesNode = valNode
}
// If the key is not one of the allowed top-level keys, treat it as a job.
if _, ok := allowedKeys[keyNode.Value]; !ok {
// Create a new Job.
job := &Job{
Name: keyNode.Value,
Line: keyNode.Line,
Column: keyNode.Column,
DependencyLines: make(map[string]struct{ Line, Column int }),
}
// Even if the job value is not a mapping node, add the job to config for further validation.
if valNode.Kind == yaml.MappingNode {
// Optionally, you can validate unknown keys inside the job mapping here.
// For example:
validateJobMapping(valNode, ve)
// Decode the job mapping.
err = valNode.Decode(job)
if err != nil {
return nil, fmt.Errorf("failed to decode job '%s': %w", job.Name, err)
}
// Process inner fields manually (e.g. dependencies, needs, only, etc.)
for d := 0; d < len(valNode.Content)-1; d += 2 {
depKey := valNode.Content[d]
depVal := valNode.Content[d+1]
switch depKey.Value {
case "dependencies":
if depVal.Kind == yaml.SequenceNode {
for k := 0; k < len(depVal.Content); k++ {
depNode := depVal.Content[k]
job.Dependencies = append(job.Dependencies, depNode.Value)
job.DependencyLines[depNode.Value] = struct {
Line int
Column int
}{depNode.Line, depNode.Column}
}
}
case "needs":
if depVal.Kind == yaml.SequenceNode {
job.NeedLines = make(map[string]struct {
Line int
Column int
})
for k := 0; k < len(depVal.Content); k++ {
needNode := depVal.Content[k]
job.Needs = append(job.Needs, needNode.Value)
job.NeedLines[needNode.Value] = struct {
Line int
Column int
}{needNode.Line, needNode.Column}
}
}
case "only":
if depVal.Kind == yaml.ScalarNode {
job.Only = depVal.Value
} else if depVal.Kind == yaml.SequenceNode {
var arr []string
for k := 0; k < len(depVal.Content); k++ {
arr = append(arr, depVal.Content[k].Value)
}
job.Only = arr
}
job.OnlyLine = depKey.Line
job.OnlyColumn = depKey.Column
case "variables":
// Заполняем поля переменных для job.
job.VariablesNode = depVal
var vars map[string]interface{}
if err := depVal.Decode(&vars); err != nil {
return nil, fmt.Errorf("failed to decode variables for job '%s': %w", job.Name, err)
}
job.Variables = vars
}
}
} else {
// If the job value is not a mapping, leave job fields as default.
}
// Save the job in config.
config.Jobs[job.Name] = job
}
}
}
return &config, nil
}
// findUnknownRootKeysWithSpan scans the mainNode for unknown keys and returns their information.
func findUnknownRootKeysWithSpan(mainNode *yaml.Node) []errors.UnknownKeyError {
allowedKeys := map[string]struct{}{
"default": {},
"include": {},
//"before_script": {},
"image": {},
"services": {},
"after_script": {},
"variables": {},
"stages": {},
"cache": {},
"workflow": {},
"jobs": {},
"changes": {},
"paths": {},
}
var unknowns []errors.UnknownKeyError
if mainNode.Kind != yaml.MappingNode {
return unknowns
}
for i := 0; i < len(mainNode.Content)-1; i += 2 {
keyNode := mainNode.Content[i]
if _, ok := allowedKeys[keyNode.Value]; !ok && keyNode.Value == "before_script" {
endLine, endCol := keyNode.Line, keyNode.Column+len(keyNode.Value)
unknowns = append(unknowns, errors.UnknownKeyError{
Key: keyNode.Value,
Line: keyNode.Line,
Column: keyNode.Column,
EndLine: endLine,
EndColumn: endCol,
})
}
}
return unknowns
}