diff --git a/errors/enum.go b/errors/enum.go index 7ff4a64..a1bb515 100644 --- a/errors/enum.go +++ b/errors/enum.go @@ -21,6 +21,7 @@ const ( ErrNoVisibleJob ErrorCode = "NO_VISIBLE_JOB" // нет ни одной видимой задачи ErrMissingJobs ErrorCode = "MISSING_JOBS" ErrArtifactsPathsBlank ErrorCode = "ARTIFACTS_PATHS_BLANK" // отсутствует или пустой блок paths в artifacts + ErrUnknownRootKey ErrorCode = "UNKNOWN_ROOT_KEY" // Changes ErrChangesNotArrayOfStrings ErrorCode = "CHANGES_NOT_ARRAY_OF_STRINGS" diff --git a/errors/errors.go b/errors/errors.go index e64f435..bbffac6 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -15,6 +15,14 @@ type ValidationErrorDetail struct { EndColumn int // Конечный столбец } +type UnknownKeyError struct { + Key string + Line int + Column int + EndLine int + EndColumn int +} + // ValidationError представляет совокупность ошибок валидации. type ValidationError struct { Errors []ValidationErrorDetail diff --git a/gitlabcivalidator/parser.go b/gitlabcivalidator/parser.go index c7625a3..9a28f2c 100644 --- a/gitlabcivalidator/parser.go +++ b/gitlabcivalidator/parser.go @@ -2,6 +2,7 @@ package gitlabcivalidator import ( "fmt" + "gitflame.ru/CI_VLDR/errors" "gopkg.in/yaml.v3" ) @@ -29,12 +30,6 @@ func ParseGitLabCIConfig(data []byte) (*GitLabCIConfig, error) { // Основной объект — первый дочерний элемент mainNode := root.Content[0] - // Здесь выполняем проверку ключей - unknownKeys := findUnknownRootKeys(mainNode) - if len(unknownKeys) > 0 { - return nil, fmt.Errorf("contains unknown keys: %v", unknownKeys) - } - // Декодируем root-объект в структуру err = mainNode.Decode(&config) if err != nil { @@ -114,8 +109,7 @@ func ParseGitLabCIConfig(data []byte) (*GitLabCIConfig, error) { return &config, nil } -func findUnknownRootKeys(mainNode *yaml.Node) []string { - // Разрешённые ключи корневого объекта +func findUnknownRootKeysWithSpan(mainNode *yaml.Node) []errors.UnknownKeyError { allowedKeys := map[string]struct{}{ "default": {}, "include": {}, @@ -132,17 +126,23 @@ func findUnknownRootKeys(mainNode *yaml.Node) []string { "paths": {}, } - var unknownKeys []string + var unknowns []errors.UnknownKeyError if mainNode.Kind != yaml.MappingNode { - return unknownKeys + return unknowns } - // В YAML Mapping узле ключи находятся в нечетных индексах for i := 0; i < len(mainNode.Content)-1; i += 2 { keyNode := mainNode.Content[i] if _, ok := allowedKeys[keyNode.Value]; !ok { - unknownKeys = append(unknownKeys, keyNode.Value) + 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 unknownKeys + return unknowns } diff --git a/gitlabcivalidator/validator.go b/gitlabcivalidator/validator.go index 725da70..b74bdd7 100644 --- a/gitlabcivalidator/validator.go +++ b/gitlabcivalidator/validator.go @@ -3,6 +3,7 @@ package gitlabcivalidator import ( "fmt" "gitflame.ru/CI_VLDR/errors" + "gopkg.in/yaml.v3" "strings" "time" ) @@ -16,40 +17,56 @@ func computeEndPosition(startLine, startColumn int, text string) (endLine, endCo return startLine, startColumn + len(text) } -// ValidateGitLabCIFile — главная точка входа в библиотеку. -// Принимает на вход содержимое GitLab CI-файла (в формате YAML) и опции валидации. -// Возвращает ошибку, если есть проблемы в конфигурации (или nil, если всё хорошо). +// ValidateGitLabCIFile – главная точка входа в библиотеку. func ValidateGitLabCIFile(content string, opts ValidationOptions) error { validationErr := &errors.ValidationError{} - // 1) Проверяем, что содержимое не пустое if len(content) == 0 { validationErr.AddDetailWithSpan(errors.ErrEmptyFile, "Please provide content of .gitflame-ci.yml", 0, 0, 0, 0) return validationErr } - // 2) Парсим YAML - cfg, err := ParseGitLabCIConfig([]byte(content)) + // Повторно распарсить YAML, чтобы получить узел и позиционную информацию + var root yaml.Node + err := yaml.Unmarshal([]byte(content), &root) if err != nil { validationErr.AddDetailWithSpan(errors.ErrYamlSyntax, fmt.Sprintf("ошибка синтаксического анализа YAML: %v", err), 0, 0, 0, 0) return validationErr } + if len(root.Content) == 0 { + validationErr.AddDetailWithSpan(errors.ErrYamlSyntax, + "файл .gitlab-ci.yml пуст или не содержит допустимых данных", + 0, 0, 0, 0) + return validationErr + } + if root.Kind != yaml.DocumentNode || len(root.Content) == 0 { + validationErr.AddDetailWithSpan(errors.ErrYamlSyntax, + "некорректный формат YAML: ожидался корневой объект", + 0, 0, 0, 0) + return validationErr + } - // 3) (Опционально) Проверяем SHA проекта, - // если включена опция и "проект с заданным SHA существует, но SHA не содержится ни в одной ветке/теге" - //if opts.VerifyProjectSHA && opts.ProjectSHA != "" { - // // Заглушка: предположим, что проект существует, - // // а также предположим, что SHA нигде не встречается - // // (в реальном мире тут нужно обращаться к GitLab API). - // shaExists := checkSHAExistsInBranchesOrTags(opts.ProjectSHA) - // if !shaExists { - // validationErr.Add(fmt.Errorf("указанный SHA (%s) не содержится ни в одной ветке или теге", opts.ProjectSHA)) - // } - //} + // Получаем корневой узел + mainNode := root.Content[0] + // Проверяем неизвестные ключи и добавляем ошибки в объект валидации + unknowns := findUnknownRootKeysWithSpan(mainNode) + for _, unk := range unknowns { + validationErr.AddDetailWithSpan(errors.ErrUnknownRootKey, + fmt.Sprintf("unexpected key: '%s'", unk.Key), + unk.Line, unk.Column, unk.EndLine, unk.EndColumn) + } + + // Парсим YAML в структуру конфигурации + cfg, err := ParseGitLabCIConfig([]byte(content)) + if err != nil { + validationErr.AddDetailWithSpan(errors.ErrYamlSyntax, + fmt.Sprintf("ошибка синтаксического анализа YAML: %v", err), + 0, 0, 0, 0) + return validationErr + } - // 4) Валидируем структуру конфигурации validateRootObject(cfg, validationErr) validateJobs(cfg, validationErr) validateChanges(cfg, validationErr) @@ -66,30 +83,21 @@ func ValidateGitLabCIFile(content string, opts ValidationOptions) error { return nil } -// checkSHAExistsInBranchesOrTags - фейковая функция для проверки SHA. -// В реальном проекте здесь вы бы обращались к GitLab API / репозиторию. func checkSHAExistsInBranchesOrTags(sha string) bool { - // Для демонстрации пусть всегда возвращает false, - // т.е. "SHA не найден". return false } -// validateRootObject проверяет допустимые ключи на корневом уровне. func validateRootObject(cfg *GitLabCIConfig, ve *errors.ValidationError) { validateBeforeScript(cfg.BeforeScript, ve) } -// validateBeforeScript проверяет, что поле before_script представлено строкой -// или вложенным массивом строк (до 10 уровней вложенности). func validateBeforeScript(before interface{}, ve *errors.ValidationError) { if before == nil { return } - // Если это строка — всё ок. if _, ok := before.(string); ok { return } - // Если это массив, проверяем вложенность. if arr, ok := before.([]interface{}); ok { if !validateNestedStringArray(arr, 1, 10) { ve.AddDetailWithSpan(errors.ErrBeforeScriptInvalid, @@ -103,18 +111,15 @@ func validateBeforeScript(before interface{}, ve *errors.ValidationError) { } } -// validateNestedStringArray рекурсивно проверяет, что все элементы массива являются строками -// или вложенными массивами строк и что глубина вложенности не превышает maxLevel. func validateNestedStringArray(arr []interface{}, currentLevel, maxLevel int) bool { if currentLevel > maxLevel { return false } for _, item := range arr { - switch v := item.(type) { + switch item.(type) { case string: - // ок case []interface{}: - if !validateNestedStringArray(v, currentLevel+1, maxLevel) { + if !validateNestedStringArray(item.([]interface{}), currentLevel+1, maxLevel) { return false } default: @@ -124,16 +129,13 @@ func validateNestedStringArray(arr []interface{}, currentLevel, maxLevel int) bo return true } -// validateService проверяет, что конфигурация сервиса имеет тип string или map[string]interface{}. func validateService(cfg *GitLabCIConfig, ve *errors.ValidationError) { if cfg.Services == nil { return } switch cfg.Services.(type) { case string: - // Корректно case map[string]interface{}: - // Корректно default: ve.AddDetailWithSpan(errors.ErrServiceInvalid, "config should be a hash or a string", @@ -141,7 +143,6 @@ func validateService(cfg *GitLabCIConfig, ve *errors.ValidationError) { } } -// validateStages проверяет, что каждая стадия задана как непустая строка. func validateStages(cfg *GitLabCIConfig, ve *errors.ValidationError) { for i, st := range cfg.Stages { if strings.TrimSpace(st) == "" { @@ -152,9 +153,6 @@ func validateStages(cfg *GitLabCIConfig, ve *errors.ValidationError) { } } -// validateVariables проверяет корректность описания переменных. -// Если значение переменной — скаляр, оно должно быть строкой, числом или булевым значением. -// Если переменная задана как хэш, допустимы ключи: value, description, expand, options. func validateVariables(cfg *GitLabCIConfig, ve *errors.ValidationError) { if cfg.Variables == nil { return @@ -172,7 +170,7 @@ func validateVariables(cfg *GitLabCIConfig, ve *errors.ValidationError) { } for varName, val := range varsMap { if isScalar(val) { - // всё ок + // ok } else if nested, ok := val.(map[string]interface{}); ok { invalidKeys := []string{} for k := range nested { @@ -193,7 +191,6 @@ func validateVariables(cfg *GitLabCIConfig, ve *errors.ValidationError) { } } -// isScalar возвращает true, если значение является скаляром (string, bool, числовым типом). func isScalar(val interface{}) bool { switch val.(type) { case string, bool, int, int64, float64, float32: @@ -203,132 +200,117 @@ func isScalar(val interface{}) bool { } } -// validateJobs проводит серию проверок для job'ов. func validateJobs(cfg *GitLabCIConfig, ve *errors.ValidationError) { - // Если jobs отсутствуют, возвращаем ошибку с позицией 0,0 (так как позиция не определена) if cfg.Jobs == nil || len(cfg.Jobs) == 0 { ve.AddDetailWithSpan(errors.ErrMissingJobs, "отсутствуют job'ы (jobs) в конфигурации", 0, 0, 0, 0) return } - // Формируем список разрешённых стадий из cfg.Stages stagesAllowed := make(map[string]struct{}) for _, st := range cfg.Stages { stagesAllowed[st] = struct{}{} } visibleCount := 0 - - // Для проверки циклов зависимости можно собрать dependency graph (пока не используем для цикла) dependencyGraph := make(map[string][]string) - // Обходим все job'ы for _, job := range cfg.Jobs { - // a) Имя задачи не может быть пустым + // a) Проверка имени задачи if job.Name == "" { - ve.AddDetailWithSpan(errors.ErrJobNameBlank, "job name can't be blank", job.Line, job.Column, job.Line, job.Column) + ve.AddDetailWithSpan(errors.ErrJobNameBlank, + "job name can't be blank", + job.Line, job.Column, job.Line, job.Column+1) continue } - - // b) Если имя не начинается с точки — задача считается видимой if job.Name[0] != '.' { visibleCount++ } - //// c) Проверяем обязательное наличие script или run - //scriptPresent := job.Script != nil - //if !scriptPresent && job.Run == nil { - // ve.AddDetail(errors.ErrMissingScript, - // fmt.Sprintf("задача '%s' не содержит script или run", job.Name), - // job.Line, job.Column) - //} - - // d) Нельзя использовать одновременно script и run + // d) Проверяем наличие хотя бы одного из script, run или trigger if job.Script == nil && job.Run == nil && job.Trigger == nil { - ve.AddDetail(errors.ErrMissingScript, + ve.AddDetailWithSpan(errors.ErrMissingScript, fmt.Sprintf("задача '%s' не содержит script, run или trigger", job.Name), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+1) } - // e) Если указан stage, он должен присутствовать в списке разрешённых + // e) Проверка stage if job.Stage != "" { if _, ok := stagesAllowed[job.Stage]; !ok { - ve.AddDetail(errors.ErrJobStageNotExist, + ve.AddDetailWithSpan(errors.ErrJobStageNotExist, fmt.Sprintf("задача '%s': выбранный stage '%s' не существует; доступные stage: %v", job.Name, job.Stage, cfg.Stages), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+len(job.Stage)) } } - // f) Проверяем, что все зависимости (dependencies) ссылаются на существующие job'ы + // f) Проверка dependencies dependencyGraph[job.Name] = append(dependencyGraph[job.Name], job.Dependencies...) for _, dep := range job.Dependencies { depPos, _ := job.DependencyLines[dep] if _, ok := cfg.Jobs[dep]; !ok { - ve.AddDetail(errors.ErrUndefinedDependency, - fmt.Sprintf("задача '%s' (строка %d, символ %d) имеет зависимость '%s' (строка %d, символ %d), которой нет среди job'ов", - job.Name, job.Line, job.Column, dep, depPos.Line, depPos.Column), - job.Line, job.Column) + endLine, endCol := computeEndPosition(depPos.Line, depPos.Column, dep) + ve.AddDetailWithSpan(errors.ErrUndefinedDependency, + fmt.Sprintf("задача '%s' (Line %d, Col %d) имеет зависимость '%s' (Line %d, Col %d-%d), которой нет среди job'ов", + job.Name, job.Line, job.Column, dep, depPos.Line, depPos.Column, endCol), + job.Line, job.Column, endLine, endCol) } else { - // Проверяем порядок stage — зависимость не должна идти позже if !checkStageOrder(cfg, dep, job.Name) { - ve.AddDetail(errors.ErrInvalidStageOrder, - fmt.Sprintf("задача '%s' имеет зависимость '%s' со стадией, идущей позже, чем '%s'", job.Name, dep, job.Name), - job.Line, job.Column) + ve.AddDetailWithSpan(errors.ErrInvalidStageOrder, + fmt.Sprintf("задача '%s' имеет зависимость '%s' со стадией, идущей позже, чем '%s'", + job.Name, dep, job.Name), + job.Line, job.Column, job.Line, job.Column+len(dep)) } } } - // g) Проверяем блок needs + // g) Проверка блока needs if len(job.Needs) > 0 { needNames := make(map[string]struct{}) for _, nd := range job.Needs { - // Не должно быть дублирующихся записей в needs + // Проверка дублирования if _, exist := needNames[nd]; exist { - // Здесь, например, выделяем весь фрагмент с нужной зависимостью. - // Предположим, что мы можем вычислить окончание как: - endCol := job.Column + len(nd) + ndStartLine, ndStartCol := job.NeedLines[nd].Line, job.NeedLines[nd].Column + ndEndLine, ndEndCol := computeEndPosition(ndStartLine, ndStartCol, nd) ve.AddDetailWithSpan(errors.ErrDuplicateNeeds, fmt.Sprintf("задача '%s' содержит дублирующуюся зависимость '%s' в needs", job.Name, nd), - job.Line, job.Column, job.Line, endCol) + ndStartLine, ndStartCol, ndEndLine, ndEndCol) } else { needNames[nd] = struct{}{} } - // Имя в needs не должно быть пустым + // Если имя пустое if nd == "" { ve.AddDetailWithSpan(errors.ErrJobNameBlank, fmt.Sprintf("задача '%s' имеет needs с пустым именем задачи", job.Name), - job.Line, job.Column, job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+1) } - // Проверяем максимальную длину имени + // Проверка максимальной длины if len(nd) > MaxJobNameLength { - endCol := job.Column + len(nd) + ndStartLine, ndStartCol := job.NeedLines[nd].Line, job.NeedLines[nd].Column + ndEndLine, ndEndCol := computeEndPosition(ndStartLine, ndStartCol, nd) ve.AddDetailWithSpan(errors.ErrNeedNameTooLong, fmt.Sprintf("задача '%s' содержит needs '%s', длина имени задачи превышает %d", job.Name, nd, MaxJobNameLength), - job.Line, job.Column, job.Line, endCol) + ndStartLine, ndStartCol, ndEndLine, ndEndCol) } - // Каждая задача, указанная в needs, должна существовать + // Проверяем, что задача из needs существует needPos, posExists := job.NeedLines[nd] if !posExists { needPos = struct{ Line, Column int }{Line: job.Line, Column: job.Column} } if _, ok := cfg.Jobs[nd]; !ok { - endCol := needPos.Column + len(nd) + ndEndLine, ndEndCol := computeEndPosition(needPos.Line, needPos.Column, nd) ve.AddDetailWithSpan(errors.ErrUndefinedNeed, - fmt.Sprintf("задача '%s' (Line %d, Col %d-%d) ссылается на несуществующую задачу '%s'", job.Name, needPos.Line, needPos.Column, endCol, nd), - job.Line, job.Column, - needPos.Line, endCol) + fmt.Sprintf("задача '%s' (Line %d, Col %d-%d) ссылается на несуществующую задачу '%s'", job.Name, needPos.Line, needPos.Column, ndEndCol, nd), + needPos.Line, needPos.Column, ndEndLine, ndEndCol) } else if !checkStageOrder(cfg, nd, job.Name) { - endCol := needPos.Column + len(nd) + ndEndLine, ndEndCol := computeEndPosition(needPos.Line, needPos.Column, nd) ve.AddDetailWithSpan(errors.ErrInvalidStageOrder, fmt.Sprintf("задача '%s' (Line %d, Col %d) имеет need '%s' (Line %d, Col %d-%d) со стадией, идущей позже, чем '%s'", - job.Name, job.Line, job.Column, nd, needPos.Line, needPos.Column, endCol, job.Name), - job.Line, job.Column, - needPos.Line, endCol) + job.Name, job.Line, job.Column, nd, needPos.Line, needPos.Column, ndEndCol, job.Name), + needPos.Line, needPos.Column, ndEndLine, ndEndCol) } - // Добавляем запись needs в граф зависимостей + dependencyGraph[job.Name] = append(dependencyGraph[job.Name], nd) } } @@ -337,52 +319,40 @@ func validateJobs(cfg *GitLabCIConfig, ve *errors.ValidationError) { validateJobDelayedParameters(job, ve) validateJobDependenciesNeedsConsistency(job, ve) validateJobRules(job, ve) - } - // Проверка: должна быть хотя бы одна видимая задача if visibleCount == 0 { - ve.AddDetail(errors.ErrNoVisibleJob, - "в разделе jobs не обнаружено ни одной «видимой» задачи", 0, 0) + ve.AddDetailWithSpan(errors.ErrNoVisibleJob, + "в разделе jobs не обнаружено ни одной «видимой» задачи", + 0, 0, 0, 0) } - // После обхода всех job проверяем граф зависимостей на циклы. cycle := detectCycles(dependencyGraph) if len(cycle) > 0 { - ve.AddDetail(errors.ErrCyclicDependency, + ve.AddDetailWithSpan(errors.ErrCyclicDependency, fmt.Sprintf("проверка циклических зависимостей: обнаружен цикл между job: %v", cycle), - 0, 0) + 0, 0, 0, 0) } } func validateJobArtifacts(job *Job, ve *errors.ValidationError) { if job.Artifacts != nil { - // Если paths не указаны или массив пустой, возвращаем ошибку if len(job.Artifacts.Paths) == 0 { - ve.AddDetail( - errors.ErrArtifactsPathsBlank, + ve.AddDetailWithSpan(errors.ErrArtifactsPathsBlank, fmt.Sprintf("задача '%s': artifacts paths can't be blank", job.Name), - job.Line, job.Column, - ) + job.Line, job.Column, job.Line, job.Column+1) } } } -// checkStageOrder проверяет, что stage у jobDep не позже, чем stage у jobName func checkStageOrder(cfg *GitLabCIConfig, jobDep, jobName string) bool { depStage := cfg.Jobs[jobDep].Stage jobStage := cfg.Jobs[jobName].Stage - - // Если stage пустые, условимся, что порядок "любопытен", - // но в реальном GitLab это тоже может вызывать ошибки. if depStage == "" || jobStage == "" { - return true // условно пропустим + return true } - allStages := cfg.Stages - - depIndex := -1 - jobIndex := -1 + depIndex, jobIndex := -1, -1 for i, st := range allStages { if st == depStage { depIndex = i @@ -391,13 +361,9 @@ func checkStageOrder(cfg *GitLabCIConfig, jobDep, jobName string) bool { jobIndex = i } } - - // Если не нашли stage, тоже пропустим, ибо сам факт уже поднимался выше if depIndex == -1 || jobIndex == -1 { return true } - - // Зависимость не должна иметь stage с индексом больше, чем у job return depIndex <= jobIndex } @@ -408,38 +374,38 @@ func validateChanges(cfg *GitLabCIConfig, ve *errors.ValidationError) { switch v := cfg.Changes.(type) { case []interface{}: if len(v) > 50 { - ve.AddDetail(errors.ErrChangesTooManyEntries, "has too many entries (maximum 50)", 0, 0) + ve.AddDetailWithSpan(errors.ErrChangesTooManyEntries, "has too many entries (maximum 50)", 0, 0, 0, 0) } for _, item := range v { if _, ok := item.(string); !ok { - ve.AddDetail(errors.ErrChangesNotArrayOfStrings, "changes config should be an array of strings", 0, 0) + ve.AddDetailWithSpan(errors.ErrChangesNotArrayOfStrings, "changes config should be an array of strings", 0, 0, 0, 0) break } } case map[string]interface{}: paths, ok := v["paths"] if !ok { - ve.AddDetail(errors.ErrChangesMissingPaths, "changes config hash must contain key 'paths'", 0, 0) + ve.AddDetailWithSpan(errors.ErrChangesMissingPaths, "changes config hash must contain key 'paths'", 0, 0, 0, 0) } else { arr, ok := paths.([]interface{}) if !ok { - ve.AddDetail(errors.ErrChangesNotArrayOfStrings, "changes config should be an array of strings", 0, 0) + ve.AddDetailWithSpan(errors.ErrChangesNotArrayOfStrings, "changes config should be an array of strings", 0, 0, 0, 0) } else { if len(arr) > 50 { - ve.AddDetail(errors.ErrChangesTooManyEntries, "has too many entries (maximum 50)", 0, 0) + ve.AddDetailWithSpan(errors.ErrChangesTooManyEntries, "has too many entries (maximum 50)", 0, 0, 0, 0) } for _, item := range arr { if _, ok := item.(string); !ok { - ve.AddDetail(errors.ErrChangesNotArrayOfStrings, "changes config should be an array of strings", 0, 0) + ve.AddDetailWithSpan(errors.ErrChangesNotArrayOfStrings, "changes config should be an array of strings", 0, 0, 0, 0) break } } } } case string: - ve.AddDetail(errors.ErrChangesInvalidType, "should be an array or a hash", 0, 0) + ve.AddDetailWithSpan(errors.ErrChangesInvalidType, "should be an array or a hash", 0, 0, 0, 0) default: - ve.AddDetail(errors.ErrChangesInvalidType, "should be an array or a hash", 0, 0) + ve.AddDetailWithSpan(errors.ErrChangesInvalidType, "should be an array or a hash", 0, 0, 0, 0) } } @@ -449,47 +415,44 @@ func validatePaths(cfg *GitLabCIConfig, ve *errors.ValidationError) { } arr, ok := cfg.Paths.([]interface{}) if !ok { - ve.AddDetail(errors.ErrPathsNotArrayOfStrings, "paths config should be an array of strings", 0, 0) + ve.AddDetailWithSpan(errors.ErrPathsNotArrayOfStrings, "paths config should be an array of strings", 0, 0, 0, 0) return } for _, item := range arr { if _, ok := item.(string); !ok { - ve.AddDetail(errors.ErrPathsNotArrayOfStrings, "paths config should be an array of strings", 0, 0) + ve.AddDetailWithSpan(errors.ErrPathsNotArrayOfStrings, "paths config should be an array of strings", 0, 0, 0, 0) break } } } -// validateJobDelayedParameters проверяет параметры для отложенных (delayed) задач. func validateJobDelayedParameters(job *Job, ve *errors.ValidationError) { if job.When == "delayed" { if job.StartIn == "" { - ve.AddDetail(errors.ErrStartInMissing, + ve.AddDetailWithSpan(errors.ErrStartInMissing, fmt.Sprintf("задача '%s': start_in should be specified for delayed job", job.Name), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+1) } else { d, err := time.ParseDuration(job.StartIn) if err != nil { - ve.AddDetail(errors.ErrStartInInvalid, + ve.AddDetailWithSpan(errors.ErrStartInInvalid, fmt.Sprintf("задача '%s': job start in should be a duration", job.Name), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+1) } else if d > 7*24*time.Hour { - ve.AddDetail(errors.ErrStartInTooLong, + ve.AddDetailWithSpan(errors.ErrStartInTooLong, fmt.Sprintf("задача '%s': job start in should not exceed the limit", job.Name), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+1) } } } else { - // Если задача не отложенная, start_in должен быть пустым if job.StartIn != "" { - ve.AddDetail(errors.ErrStartInMustBeBlank, + ve.AddDetailWithSpan(errors.ErrStartInMustBeBlank, fmt.Sprintf("задача '%s': job start in must be blank when not delayed", job.Name), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+1) } } } -// validateJobDependenciesNeedsConsistency убеждается, что все зависимости присутствуют в needs. func validateJobDependenciesNeedsConsistency(job *Job, ve *errors.ValidationError) { if len(job.Dependencies) > 0 && len(job.Needs) > 0 { needNames := make(map[string]struct{}) @@ -498,9 +461,9 @@ func validateJobDependenciesNeedsConsistency(job *Job, ve *errors.ValidationErro } for _, dep := range job.Dependencies { if _, ok := needNames[dep]; !ok { - ve.AddDetail(errors.ErrDependencyNotInNeeds, + ve.AddDetailWithSpan(errors.ErrDependencyNotInNeeds, fmt.Sprintf("задача '%s': dependency '%s' should be part of needs", job.Name, dep), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+len(dep)) } } } @@ -508,44 +471,40 @@ func validateJobDependenciesNeedsConsistency(job *Job, ve *errors.ValidationErro func validateJobRules(job *Job, ve *errors.ValidationError) { if job.Rules != nil { - // Если rules используется, не должно быть only или except if job.Only != nil && job.Except != nil { - ve.AddDetail(errors.ErrRulesOnlyExcept, + ve.AddDetailWithSpan(errors.ErrRulesOnlyExcept, fmt.Sprintf("задача '%s': may not be used with rules: only, except", job.Name), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+1) } else if job.Only != nil { - ve.AddDetail(errors.ErrRulesOnly, + ve.AddDetailWithSpan(errors.ErrRulesOnly, fmt.Sprintf("задача '%s': may not be used with rules: only", job.Name), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+1) } else if job.Except != nil { - ve.AddDetail(errors.ErrRulesExcept, + ve.AddDetailWithSpan(errors.ErrRulesExcept, fmt.Sprintf("задача '%s': may not be used with rules: except", job.Name), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+1) } - // Если в секции rules присутствует ключ if, проверим его тип и синтаксис if rulesMap, ok := job.Rules.(map[string]interface{}); ok { if ifVal, exists := rulesMap["if"]; exists { if ifStr, ok := ifVal.(string); ok { if !validateExpressionSyntax(ifStr) { - ve.AddDetail(errors.ErrInvalidExpressionSyntax, + ve.AddDetailWithSpan(errors.ErrInvalidExpressionSyntax, fmt.Sprintf("задача '%s': invalid expression syntax", job.Name), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+len(ifStr)) } } else { - ve.AddDetail(errors.ErrInvalidExpressionSyntax, + ve.AddDetailWithSpan(errors.ErrInvalidExpressionSyntax, fmt.Sprintf("задача '%s': 'if' in rules should be a string", job.Name), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+1) } } - // Проверим, что ключ allow_failure, если присутствует, имеет булевое значение if af, exists := rulesMap["allow_failure"]; exists { if _, ok := af.(bool); !ok { - ve.AddDetail(errors.ErrBooleanValue, + ve.AddDetailWithSpan(errors.ErrBooleanValue, fmt.Sprintf("задача '%s': allow_failure in rules should be a boolean value", job.Name), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+1) } } - // Проверка допустимых ключей в rules allowed := map[string]struct{}{ "if": {}, "when": {}, @@ -556,34 +515,27 @@ func validateJobRules(job *Job, ve *errors.ValidationError) { } for key := range rulesMap { if _, ok := allowed[key]; !ok { - ve.AddDetail(errors.ErrUnknownRulesKey, + ve.AddDetailWithSpan(errors.ErrUnknownRulesKey, fmt.Sprintf("задача '%s': unknown key in rules: %s", job.Name, key), - job.Line, job.Column) + job.Line, job.Column, job.Line, job.Column+len(key)) } } } } } -// validateExpressionSyntax выполняет базовую проверку синтаксиса выражения. -// В реальной реализации здесь может быть полноценный парсер. func validateExpressionSyntax(expr string) bool { - // Для демонстрации считаем, что пустая строка или отсутствие каких-либо специальных символов — это ошибка. return expr != "" } -// detectCycles принимает граф зависимостей в виде мапы: job -> список зависимых job, -// и возвращает список job, участвующих в цикле (если он обнаружен). func detectCycles(dependencyGraph map[string][]string) []string { visited := make(map[string]bool) recStack := make(map[string]bool) var cycle []string - // Рекурсивная функция для обхода графа. var dfs func(string) bool dfs = func(node string) bool { if recStack[node] { - // Если node уже в рекурсивном стеке, значит обнаружен цикл. cycle = append(cycle, node) return true } @@ -599,23 +551,20 @@ func detectCycles(dependencyGraph map[string][]string) []string { return true } } - recStack[node] = false return false } - // Запускаем DFS для каждой job. for node := range dependencyGraph { if !visited[node] { if dfs(node) { - break // если цикл обнаружен, прерываем обход + break } } } return cycle } -// Функция для валидации include-правил func validateIncludeRules(cfg *GitLabCIConfig, ve *errors.ValidationError) { if cfg.Include == nil { return @@ -627,39 +576,31 @@ func validateIncludeRules(cfg *GitLabCIConfig, ve *errors.ValidationError) { for _, item := range v { if m, ok := item.(map[string]interface{}); ok { validateIncludeRuleMap(m, ve) - } else { - // Если элемент не является картой, можно считать его допустимым, - // либо добавить дополнительную проверку при необходимости. } } - default: - // Если include задан не как карта и не как массив, пропускаем проверку. } } -// Функция, которая проверяет отдельную карту include-правила func validateIncludeRuleMap(m map[string]interface{}, ve *errors.ValidationError) { - // Проверяем ключи "exists" и "changes" keysToCheck := []string{"exists", "changes"} for _, key := range keysToCheck { if val, exists := m[key]; exists { - switch v := val.(type) { + switch val.(type) { case string: - // Корректное значение – строка. + // ok case []interface{}: - // Проверяем, что все элементы массива – строки. - for _, item := range v { + for _, item := range val.([]interface{}) { if _, ok := item.(string); !ok { - ve.AddDetail(errors.ErrIncludeRulesInvalid, + ve.AddDetailWithSpan(errors.ErrIncludeRulesInvalid, fmt.Sprintf("include rule key '%s' should be an array of strings", key), - 0, 0) + 0, 0, 0, 0) break } } default: - ve.AddDetail(errors.ErrIncludeRulesInvalid, + ve.AddDetailWithSpan(errors.ErrIncludeRulesInvalid, fmt.Sprintf("include rule key '%s' should be a string or an array of strings", key), - 0, 0) + 0, 0, 0, 0) } } } diff --git a/main.go b/main.go index 8061986..75136b5 100644 --- a/main.go +++ b/main.go @@ -24,23 +24,27 @@ jobs: script: - echo "Testing..." ` - + // Тип, ссылка и текст ошибки, показатели где нахоидтся ошибка... // Некорректный YAML — отсутствует видимая задача, есть только скрытая invalidYAML := ` stages: - build - test +holy_cow: + - ass +asdasd: + - sasafafasf +asdasdd: + - asd jobs: build_job: - stage: build + stage: build2 script: - echo "Building..." needs: ["test_job2"] test_job2: stage: test - script: - - echo "Testing..." - needs: ["build_job", "test_job2", "test_job3"] + needs: ["build_job", "test_job2", "test_job3", "test_job4"] ` fmt.Println("=== Пример 1: Корректный YAML ===")