From d7ae9bea8adcdd9cc2837d04c386e9c2fafd044e Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 17 Mar 2025 20:26:14 +0300 Subject: [PATCH] More errors, some fixes --- errors/enum.go | 116 ++++++++++++------------ errors/messages.go | 2 +- gitlabcivalidator/models.go | 28 +++--- gitlabcivalidator/parser.go | 58 +++++++----- gitlabcivalidator/validator.go | 156 ++++++++++++++++++++++++++------- main.go | 66 -------------- 6 files changed, 242 insertions(+), 184 deletions(-) diff --git a/errors/enum.go b/errors/enum.go index 5d27037..080459a 100644 --- a/errors/enum.go +++ b/errors/enum.go @@ -4,67 +4,74 @@ package errors type ErrorCode string const ( - ErrEmptyFile ErrorCode = "EMPTY_FILE" // файл пуст или отсутствует - ErrYamlSyntax ErrorCode = "YAML_SYNTAX_ERROR" // ошибка синтаксического анализа YAML + // General Errors + ErrEmptyFile ErrorCode = "FileIsEmpty" // The file is empty or missing + ErrYamlSyntax ErrorCode = "YAMLSyntaxError" // YAML syntax error - ErrJobNameBlank ErrorCode = "JOB_NAME_BLANK" // имя задачи пустое - ErrJobNameTooLong ErrorCode = "JOB_NAME_TOO_LONG" - ErrMissingScript ErrorCode = "MISSING_SCRIPT" // отсутствует обязательное поле script (или run) - ErrBothScriptAndRun ErrorCode = "BOTH_SCRIPT_RUN" // одновременно присутствуют script и run - ErrJobStageNotExist ErrorCode = "JOB_STAGE_NOT_EXIST" // указан stage, которого нет в списке разрешённых - ErrUndefinedDependency ErrorCode = "UNDEFINED_DEPENDENCY" // зависимость не определена в списке job'ов - ErrInvalidStageOrder ErrorCode = "INVALID_STAGE_ORDER" // зависимость имеет stage, который идёт позже, чем у задачи - ErrDuplicateNeeds ErrorCode = "DUPLICATE_NEEDS" // дублирующиеся записи в needs - ErrUndefinedNeed ErrorCode = "UNDEFINED_NEED" // need ссылается на несуществующую задачу - ErrNeedNameTooLong ErrorCode = "NEED_NAME_TOO_LONG" // имя need превышает допустимую длину - ErrNoVisibleJob ErrorCode = "NO_VISIBLE_JOB" // нет ни одной видимой задачи - ErrMissingJobs ErrorCode = "MISSING_JOBS" - ErrArtifactsPathsBlank ErrorCode = "ARTIFACTS_PATHS_BLANK" // отсутствует или пустой блок paths в artifacts - ErrUnknownRootKey ErrorCode = "UNKNOWN_ROOT_KEY" - ErrInvalidWhen ErrorCode = "INVALID_WHEN" - ErrInvalidOnly ErrorCode = "INVALID_ONLY" - ErrUnknownKey ErrorCode = "UNKNOWN_KEY" - ErrMissingStage ErrorCode = "MISSING_STAGE" - ErrInvalidChanges ErrorCode = "INVALID_CHANGES" - ErrInvalidRulesFormat ErrorCode = "INVALID_RULES_FORMAT" + // Job-related Errors + ErrJobNameBlank ErrorCode = "JobNameBlank" // The job name is blank + ErrJobNameTooLong ErrorCode = "JobNameTooLong" // The job name exceeds the maximum length + ErrMissingScript ErrorCode = "MissingScript" // The required field script (or run) is missing + ErrBothScriptAndRun ErrorCode = "BothScriptAndRun" // Both script and run are specified simultaneously + ErrJobStageNotExist ErrorCode = "JobStageNotExist" // The specified stage does not exist in the allowed list + ErrUndefinedDependency ErrorCode = "UndefinedDependency" // A dependency is not defined among the jobs + ErrInvalidStageOrder ErrorCode = "InvalidStageOrder" // A dependency has a stage that occurs later than the job's stage + ErrDuplicateNeeds ErrorCode = "DuplicateNeeds" // Duplicate entries found in needs + ErrUndefinedNeed ErrorCode = "UndefinedNeed" // A need refers to a non-existent job + ErrNeedNameTooLong ErrorCode = "NeedNameTooLong" // The need name exceeds the allowed length + ErrNoVisibleJob ErrorCode = "NoVisibleJob" // There are no visible jobs + ErrMissingJobs ErrorCode = "MissingJobs" // No jobs defined + ErrArtifactsPathsBlank ErrorCode = "ArtifactsPathsBlank" // The artifacts paths block is missing or empty + ErrUnknownRootKey ErrorCode = "UnknownRootKey" // An unknown key was found at the root level + ErrInvalidWhen ErrorCode = "InvalidWhen" // The value of 'when' is invalid + ErrInvalidOnly ErrorCode = "InvalidOnly" // The value of 'only' is invalid + ErrUnknownKey ErrorCode = "UnknownKey" // An unknown key was found + ErrMissingStage ErrorCode = "MissingStage" // The stage is missing + ErrInvalidChanges ErrorCode = "InvalidChanges" // The changes configuration is invalid + ErrInvalidRulesFormat ErrorCode = "InvalidRulesFormat" // The rules format is invalid - // Changes - ErrChangesNotArrayOfStrings ErrorCode = "CHANGES_NOT_ARRAY_OF_STRINGS" - ErrChangesInvalidType ErrorCode = "CHANGES_INVALID_TYPE" - ErrChangesTooManyEntries ErrorCode = "CHANGES_TOO_MANY_ENTRIES" - ErrChangesMissingPaths ErrorCode = "CHANGES_MISSING_PATHS" + // Changes-related Errors + ErrChangesNotArrayOfStrings ErrorCode = "ChangesNotArrayOfStrings" // Changes config should be an array of strings + ErrChangesInvalidType ErrorCode = "ChangesInvalidType" // Changes config is of an invalid type + ErrChangesTooManyEntries ErrorCode = "ChangesTooManyEntries" // Changes config has too many entries (maximum 50) + ErrChangesMissingPaths ErrorCode = "ChangesMissingPaths" // The changes config hash must contain the key 'paths' - // Paths - ErrPathsNotArrayOfStrings ErrorCode = "PATHS_NOT_ARRAY_OF_STRINGS" + // Paths-related Errors + ErrPathsNotArrayOfStrings ErrorCode = "PathsNotArrayOfStrings" // Paths config should be an array of strings - // Job delayed parameters - ErrStartInMissing ErrorCode = "START_IN_MISSING" - ErrStartInInvalid ErrorCode = "START_IN_INVALID" - ErrStartInTooLong ErrorCode = "START_IN_TOO_LONG" - ErrStartInMustBeBlank ErrorCode = "START_IN_MUST_BE_BLANK" + // Job Delayed Parameters + ErrStartInMissing ErrorCode = "StartInMissing" // For delayed jobs, start_in is missing + ErrStartInInvalid ErrorCode = "StartInInvalid" // start_in is not a valid duration + ErrStartInTooLong ErrorCode = "StartInTooLong" // start_in exceeds the allowed limit + ErrStartInMustBeBlank ErrorCode = "StartInMustBeBlank" // For non-delayed jobs, start_in must be blank - // Dependencies / Needs consistency - ErrDependencyNotInNeeds ErrorCode = "DEPENDENCY_NOT_IN_NEEDS" + // Dependencies / Needs Consistency + ErrDependencyNotInNeeds ErrorCode = "DependencyNotInNeeds" // A dependency is not included in needs - // Rules validation - ErrRulesOnlyExcept ErrorCode = "RULES_ONLY_EXCEPT" - ErrRulesOnly ErrorCode = "RULES_ONLY" - ErrRulesExcept ErrorCode = "RULES_EXCEPT" - ErrInvalidExpressionSyntax ErrorCode = "INVALID_EXPRESSION_SYNTAX" - ErrUnknownRulesKey ErrorCode = "UNKNOWN_RULES_KEY" + // Rules Validation + ErrRulesOnlyExcept ErrorCode = "RulesOnlyExcept" // Only and except cannot be used with rules + ErrRulesOnly ErrorCode = "RulesOnly" // Only cannot be used with rules + ErrRulesExcept ErrorCode = "RulesExcept" // Except cannot be used with rules + ErrInvalidExpressionSyntax ErrorCode = "InvalidExpressionSyntax" // The expression syntax is invalid + ErrUnknownRulesKey ErrorCode = "UnknownRulesKey" // An unknown key was found in rules - ErrBeforeScriptInvalid ErrorCode = "BEFORE_SCRIPT_INVALID" - ErrServiceInvalid ErrorCode = "SERVICE_INVALID" - ErrStageInvalid ErrorCode = "STAGE_INVALID" - ErrVariableInvalid ErrorCode = "VARIABLE_INVALID" - ErrVariableNameTooLong ErrorCode = "VARIABLE_NAME_TOO_LONG" - ErrInvalidStagesOrder ErrorCode = "INVALID_STAGES_ORDER" - ErrVariablesInvalid ErrorCode = "VARIABLES_INVALID" - ErrVariablesInvalidKey ErrorCode = "VARIABLES_INVALID_KEY" + // Other Job-related Errors + ErrBeforeScriptInvalid ErrorCode = "BeforeScriptInvalid" // The before_script configuration is invalid + ErrServiceInvalid ErrorCode = "ServiceInvalid" // The service configuration is invalid + ErrStageInvalid ErrorCode = "StageInvalid" // The stage configuration is invalid + ErrVariableInvalid ErrorCode = "VariableInvalid" // The variable is invalid + ErrVariableNameTooLong ErrorCode = "VariableNameTooLong" // The variable name exceeds the maximum length + ErrInvalidStagesOrder ErrorCode = "InvalidStagesOrder" // The stage order is invalid + ErrVariablesInvalid ErrorCode = "VariablesInvalid" // The variables configuration is invalid + ErrVariablesInvalidKey ErrorCode = "VariablesInvalidKey" // The variable uses invalid keys - ErrCyclicDependency ErrorCode = "CYCLIC_DEPENDENCY" // обнаружена циклическая зависимость - ErrBooleanValue ErrorCode = "BOOLEAN_VALUE" // значение должно быть булевым - ErrIncludeRulesInvalid ErrorCode = "INCLUDE_RULES_INVALID" // значение exists или changes не является строкой или массивом строк + // Additional Errors + ErrCyclicDependency ErrorCode = "CyclicDependency" // A cyclic dependency was detected + ErrBooleanValue ErrorCode = "BooleanValue" // The value must be boolean + ErrIncludeRulesInvalid ErrorCode = "IncludeRulesInvalid" // The 'exists' or 'changes' value is not a string or an array of strings + + // Style Note (Informational/Note) + ErrMissingFinalNewline ErrorCode = "MissingFinalNewline" // The file is missing a final empty newline ) // ErrorSeverity maps each error code to a severity level. @@ -114,5 +121,6 @@ var ErrorSeverity = map[ErrorCode]SeverityLevel{ // Notes (if needed, add informational codes here) ErrJobNameTooLong: Note, ErrVariableNameTooLong: Note, - ErrNeedNameTooLong: Critical, + ErrNeedNameTooLong: Note, + ErrMissingFinalNewline: Note, } diff --git a/errors/messages.go b/errors/messages.go index 82d1185..713ba1f 100644 --- a/errors/messages.go +++ b/errors/messages.go @@ -46,4 +46,4 @@ var ErrorMessages = map[ErrorCode]string{ ErrIncludeRulesInvalid: "include rule key '%s' should be a string or an array of strings", ErrVariableInvalid: "variable '%s' must be a scalar or a map", ErrInvalidStagesOrder: "job '%s' (Line %d, Col %d) has need '%s' (Line %d, Col %d-%d) with a stage occurring later than '%s'", -} + ErrMissingFinalNewline: "File should end with an empty line for better style"} diff --git a/gitlabcivalidator/models.go b/gitlabcivalidator/models.go index da44170..e999c4d 100644 --- a/gitlabcivalidator/models.go +++ b/gitlabcivalidator/models.go @@ -1,5 +1,7 @@ package gitlabcivalidator +import "gopkg.in/yaml.v3" + // ValidationOptions описывает настройки валидации. type ValidationOptions struct { VerifyProjectSHA bool // нужно ли проверять SHA проекта @@ -8,17 +10,18 @@ type ValidationOptions struct { // GitLabCIConfig описывает структуру корневого объекта .gitlab-ci.yml type GitLabCIConfig struct { - Default interface{} `yaml:"default,omitempty"` - Include interface{} `yaml:"include,omitempty"` - BeforeScript interface{} `yaml:"before_script,omitempty"` - Image interface{} `yaml:"image,omitempty"` - Services interface{} `yaml:"services,omitempty"` - AfterScript interface{} `yaml:"after_script,omitempty"` - Variables interface{} `yaml:"variables,omitempty"` - Stages []string `yaml:"stages,omitempty"` - Cache interface{} `yaml:"cache,omitempty"` - Workflow interface{} `yaml:"workflow,omitempty"` - Jobs map[string]*Job `yaml:"jobs,omitempty"` + Default interface{} `yaml:"default,omitempty"` + Include interface{} `yaml:"include,omitempty"` + BeforeScript interface{} `yaml:"before_script,omitempty"` + Image interface{} `yaml:"image,omitempty"` + Services interface{} `yaml:"services,omitempty"` + AfterScript interface{} `yaml:"after_script,omitempty"` + Variables interface{} `yaml:"variables,omitempty"` + VariablesNode *yaml.Node `yaml:"-"` + Stages []string `yaml:"stages,omitempty"` + Cache interface{} `yaml:"cache,omitempty"` + Workflow interface{} `yaml:"workflow,omitempty"` + Jobs map[string]*Job `yaml:"jobs,omitempty"` Changes interface{} `yaml:"changes,omitempty"` Paths interface{} `yaml:"paths,omitempty"` @@ -51,6 +54,9 @@ type Job struct { OnlyColumn int `yaml:"-"` Except interface{} `yaml:"except,omitempty"` + Variables interface{} `yaml:"variables,omitempty"` + VariablesNode *yaml.Node `yaml:"-"` + Line int `yaml:"-"` // Строка в файле Column int `yaml:"-"` // Позиция в строке DependencyLines map[string]struct { diff --git a/gitlabcivalidator/parser.go b/gitlabcivalidator/parser.go index 0b3227d..400d685 100644 --- a/gitlabcivalidator/parser.go +++ b/gitlabcivalidator/parser.go @@ -56,6 +56,10 @@ func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConf 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. @@ -79,31 +83,34 @@ func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConf for d := 0; d < len(valNode.Content)-1; d += 2 { depKey := valNode.Content[d] depVal := valNode.Content[d+1] - if depKey.Value == "dependencies" && 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} + 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} + } } - } - if depKey.Value == "needs" && 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 { + case "needs": + if depVal.Kind == yaml.SequenceNode { + job.NeedLines = make(map[string]struct { Line int Column int - }{needNode.Line, needNode.Column} + }) + 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} + } } - } - if depKey.Value == "only" { + case "only": if depVal.Kind == yaml.ScalarNode { job.Only = depVal.Value } else if depVal.Kind == yaml.SequenceNode { @@ -115,8 +122,15 @@ func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConf } 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 } - // You can add additional manual processing here. } } else { // If the job value is not a mapping, leave job fields as default. diff --git a/gitlabcivalidator/validator.go b/gitlabcivalidator/validator.go index 5fa5de9..0d3c9fd 100644 --- a/gitlabcivalidator/validator.go +++ b/gitlabcivalidator/validator.go @@ -31,19 +31,19 @@ func ValidateGitLabCIFile(content string, opts ValidationOptions) error { err := yaml.Unmarshal([]byte(content), &root) if err != nil { validationErr.AddDetailWithSpan(errors.ErrYamlSyntax, - fmt.Sprintf("ошибка синтаксического анализа YAML: %v", err), + fmt.Sprintf("YAML parsing error: %v", err), 0, 0, 0, 0) return validationErr } if len(root.Content) == 0 { validationErr.AddDetailWithSpan(errors.ErrYamlSyntax, - "файл .gitlab-ci.yml пуст или не содержит допустимых данных", + ".gitflame-ci.yml file is empty or does not contain valid data", 0, 0, 0, 0) return validationErr } if root.Kind != yaml.DocumentNode || len(root.Content) == 0 { validationErr.AddDetailWithSpan(errors.ErrYamlSyntax, - "некорректный формат YAML: ожидался корневой объект", + "invalid YAML format: a root object was expected", 0, 0, 0, 0) return validationErr } @@ -75,6 +75,7 @@ func ValidateGitLabCIFile(content string, opts ValidationOptions) error { validateStages(cfg, validationErr) validateVariables(cfg, validationErr) validateIncludeRules(cfg, validationErr) + checkFinalEmptyLine(content, validationErr) if !validationErr.IsEmpty() { return validationErr @@ -153,46 +154,123 @@ func validateStages(cfg *GitLabCIConfig, ve *errors.ValidationError) { } } -func validateVariables(cfg *GitLabCIConfig, ve *errors.ValidationError) { - if cfg.Variables == nil { - return - } - varsMap, ok := cfg.Variables.(map[string]interface{}) - if !ok { - ve.AddDetailWithSpan(errors.ErrVariablesInvalid, errors.ErrorMessages[errors.ErrVariablesInvalid], 0, 0, 0, 0) - return - } +// validateVariablesMap проверяет карту переменных varsMap, используя данные из YAML‑узла varsNode для получения позиций. +func validateVariablesMap(varsMap map[string]interface{}, varsNode *yaml.Node, ve *errors.ValidationError, posGetter func(varName string) (int, int)) { allowedKeys := map[string]struct{}{ "value": {}, "description": {}, "expand": {}, "options": {}, } - for varName, val := range varsMap { - if isScalar(val) { - if len(varName) > MaxVariableNameLength { - ve.AddDetailWithSpan(errors.ErrVariableNameTooLong, fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableNameTooLong], varName), - 0, 0, 0, 0, + + // Проходим по всем ключам, определённым в YAML‑узле + for i := 0; i < len(varsNode.Content)-1; i += 2 { + keyNode := varsNode.Content[i] + varName := keyNode.Value + // Получаем позицию для этой переменной через posGetter. + line, col := posGetter(varName) + + // Проверяем длину имени переменной + if len(varName) > MaxVariableNameLength { + ve.AddDetailWithSpan( + errors.ErrVariableNameTooLong, + fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableNameTooLong], varName), + line, col, line, col+len(varName), + ) + } + + // Проверяем, что значение переменной присутствует и корректно + if val, exists := varsMap[varName]; exists { + if !isScalar(val) { + ve.AddDetailWithSpan( + errors.ErrVariableInvalid, + fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableInvalid], varName), + line, col, line, col+len(varName), ) } - } else if nested, ok := val.(map[string]interface{}); ok { - invalidKeys := []string{} - for k := range nested { - if _, allowed := allowedKeys[k]; !allowed { - invalidKeys = append(invalidKeys, k) + if nested, ok := val.(map[string]interface{}); ok { + var invalidKeys []string + for k := range nested { + if _, allowed := allowedKeys[k]; !allowed { + invalidKeys = append(invalidKeys, k) + } + } + if len(invalidKeys) > 0 { + ve.AddDetailWithSpan( + errors.ErrVariablesInvalidKey, + fmt.Sprintf(errors.ErrorMessages[errors.ErrVariablesInvalidKey], varName, strings.Join(invalidKeys, ", ")), + line, col, line, col+len(varName), + ) } } - if len(invalidKeys) > 0 { - ve.AddDetailWithSpan(errors.ErrVariablesInvalidKey, - fmt.Sprintf(errors.ErrorMessages[errors.ErrVariablesInvalidKey], varName, strings.Join(invalidKeys, ", ")), - 0, 0, 0, 0) + } + } +} + +func validateJobVariables(job *Job, ve *errors.ValidationError) { + if job.VariablesNode == nil { + return + } + if job.VariablesNode.Kind != yaml.MappingNode { + ve.AddDetailWithSpan( + errors.ErrVariablesInvalid, + "job variables should be a map", + job.Line, job.Column, job.Line, job.Column+1, + ) + return + } + varsMap, ok := job.Variables.(map[string]interface{}) + if !ok { + ve.AddDetailWithSpan( + errors.ErrVariablesInvalid, + "job variables should be a map", + job.Line, job.Column, job.Line, job.Column+1, + ) + return + } + validateVariablesMap(varsMap, job.VariablesNode, ve, func(varName string) (int, int) { + // Ищем переменную в узле job.VariablesNode + for i := 0; i < len(job.VariablesNode.Content)-1; i += 2 { + keyNode := job.VariablesNode.Content[i] + if keyNode.Value == varName { + return keyNode.Line, keyNode.Column } - } else { - ve.AddDetailWithSpan(errors.ErrVariableInvalid, - fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableInvalid], varName), - 0, 0, 0, 0) } + return job.Line, job.Column // если не найдено, используем позицию job + }) +} + +func validateVariables(cfg *GitLabCIConfig, ve *errors.ValidationError) { + if cfg.VariablesNode == nil { + return + } + if cfg.VariablesNode.Kind != yaml.MappingNode { + ve.AddDetailWithSpan( + errors.ErrVariablesInvalid, + errors.ErrorMessages[errors.ErrVariablesInvalid], + 0, 0, 0, 0, + ) + return } + varsMap, ok := cfg.Variables.(map[string]interface{}) + if !ok { + ve.AddDetailWithSpan( + errors.ErrVariablesInvalid, + errors.ErrorMessages[errors.ErrVariablesInvalid], + 0, 0, 0, 0, + ) + return + } + validateVariablesMap(varsMap, cfg.VariablesNode, ve, func(varName string) (int, int) { + // Ищем в YAML узле переменной с именем varName + for i := 0; i < len(cfg.VariablesNode.Content)-1; i += 2 { + keyNode := cfg.VariablesNode.Content[i] + if keyNode.Value == varName { + return keyNode.Line, keyNode.Column + } + } + return 0, 0 + }) } func isScalar(val interface{}) bool { @@ -335,6 +413,7 @@ func validateJobs(cfg *GitLabCIConfig, ve *errors.ValidationError) { validateJobRules(job, ve) validateJobWhen(job, ve) validateJobOnly(job, ve) + validateJobVariables(job, ve) } if visibleCount == 0 { @@ -798,3 +877,20 @@ func validateIncludeRuleMap(m map[string]interface{}, ve *errors.ValidationError } } } + +// checkFinalEmptyLine проверяет, что файл заканчивается пустой строкой. +func checkFinalEmptyLine(content string, ve *errors.ValidationError) { + // Если содержимое не заканчивается символом новой строки... + if !strings.HasSuffix(content, "\n") { + lines := strings.Split(content, "\n") + lastLineIndex := len(lines) + lastLineContent := lines[lastLineIndex-1] + lineNum := lastLineIndex + colNum := len(lastLineContent) + 1 + ve.AddDetailWithSpan( + errors.ErrMissingFinalNewline, + errors.ErrorMessages[errors.ErrMissingFinalNewline], + lineNum, colNum, lineNum, colNum, + ) + } +} diff --git a/main.go b/main.go index 524823a..7905807 100644 --- a/main.go +++ b/main.go @@ -1,71 +1,5 @@ package main -import ( - "bufio" - "fmt" - "os" - - "tea.gitpark.ru/Azaki/CI_VALIDATOR/gitlabcivalidator" -) - func main() { - // Корректный (упрощённый) YAML - // validYAML := ` - //stages: - // - build - // - test - //jobs: - // build_job: - // stage: build - // script: - // - echo "Building..." - // test_job: - // stage: test - // dependencies: ["build_job"] - // script: - // - echo "Testing..." - //` - // Тип, ссылка и текст ошибки, показатели где нахоидтся ошибка... - // Некорректный YAML — отсутствует видимая задача, есть только скрытая - // invalidYAML := ` - //stages: - // - build - // - test - //jobs: - // teaaa: - // build_job: - // validateJobMapping(jobValNode, ve): - // script: - // - c - // needs: ["test_job2"] - // test_job2: - // stage: test - //` - - scanner := bufio.NewScanner(os.Stdin) - var inputYAML string - for scanner.Scan() { - inputYAML += scanner.Text() + "\n" - } - - if err := scanner.Err(); err != nil { - fmt.Println("Ошибка чтения ввода:", err) - return - } - - err := gitlabcivalidator.ValidateGitLabCIFile(inputYAML, gitlabcivalidator.ValidationOptions{}) - if err != nil { - fmt.Println(err.Error()) - } else { - fmt.Println("no errors") - } - //fmt.Println("\n=== Пример 2: Некорректный YAML ===") - //err2 := gitlabcivalidator.ValidateGitLabCIFile(invalidYAML, gitlabcivalidator.ValidationOptions{}) - //if err2 != nil { - // fmt.Println("Ошибка валидации (ожидаемо для данного файла):") - // fmt.Println(err2.Error()) - //} else { - // log.Fatal("Ожидали ошибку, но её не возникло") - //} }