daveshanley / vacuum

vacuum is the worlds fastest OpenAPI 3, OpenAPI 2 / Swagger linter and quality analysis tool. Built in go, it tears through API specs faster than you can think. vacuum is compatible with Spectral rulesets and generates compatible reports.
https://quobix.com/vacuum
MIT License
488 stars 39 forks source link

Vacuum not handling YAML anchors correctly #508

Open TristanSpeakEasy opened 3 weeks ago

TristanSpeakEasy commented 3 weeks ago

Consider the two attached OpenAPI docs. One is a yaml document with anchors and the other is the same document that I have just inlined the anchors by doing a roundtrip through https://github.com/go-yaml/yaml

The below reproducible code shows that the document with anchors returns incorrect yaml nodes when present (specifically I am getting a node with no content).

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/daveshanley/vacuum/model"
    "github.com/daveshanley/vacuum/motor"
    "github.com/daveshanley/vacuum/rulesets"
    "github.com/pb33f/libopenapi"
    "github.com/pb33f/libopenapi/datamodel"
    libopenapiUtils "github.com/pb33f/libopenapi/utils"
    "gopkg.in/yaml.v3"
)

type testRule struct{}

func (r *testRule) GetSchema() model.RuleFunctionSchema {
    return model.RuleFunctionSchema{
        Name: "test",
    }
}

func (r *testRule) RunRule(nodes []*yaml.Node,
    context model.RuleFunctionContext,
) []model.RuleFunctionResult {
    results := []model.RuleFunctionResult{}

    for _, schema := range context.Index.GetAllSchemas() {
        for _, nodeType := range []string{"anyOf", "allOf", "oneOf"} {
            keyNode, valNode := libopenapiUtils.FindKeyNode(nodeType, schema.Node.Content)
            if valNode == nil {
                continue
            }

            if len(valNode.Content) == 0 {
                results = append(results, model.RuleFunctionResult{
                    Message:      "empty keyword found",
                    Path:         schema.Path + "." + nodeType,
                    RuleId:       context.Rule.Id,
                    StartNode:    keyNode,
                    EndNode:      valNode,
                    RuleSeverity: context.Rule.Severity,
                })
            }
        }
    }

    return results
}

func (r *testRule) GetCategory() string {
    return "validation"
}

func main() {
    docConf := &datamodel.DocumentConfiguration{
        AllowRemoteReferences:               true,
        AllowFileReferences:                 true,
        IgnorePolymorphicCircularReferences: true,
        IgnoreArrayCircularReferences:       true,
        ExtractRefsSequentially:             true,
    }

    // data, err := os.ReadFile("openapi.yaml")
    data, err := os.ReadFile("openapi-inlined.yaml")
    if err != nil {
        log.Fatalf("error: %v", err)
    }

    d, err := libopenapi.NewDocumentWithConfiguration(data, docConf)
    if err != nil {
        log.Fatalf("error: %v", err)
    }

    ex := &motor.RuleSetExecution{
        RuleSet: &rulesets.RuleSet{
            Rules: map[string]*model.Rule{
                "test": {
                    Name:         "test",
                    Id:           "test",
                    Given:        "$",
                    Resolved:     false,
                    Severity:     "error",
                    Formats:      model.OAS3AllFormat,
                    RuleCategory: model.RuleCategories[model.CategoryValidation],
                    Type:         rulesets.Validation,
                    Then: model.RuleAction{
                        Function: "test",
                    },
                },
            },
        },
        Document: d,
        PanicFunction: func(err any) {
            log.Fatalf("error: %v", err)
        },
        CustomFunctions: map[string]model.RuleFunction{
            "test": &testRule{},
        },
        AllowLookup: true,
    }

    results := motor.ApplyRulesToRuleSet(ex)

    for _, result := range results.Results {
        log.Printf("result: %v", result)
    }

    if len(results.Results) > 0 {
        log.Fatalf("errors found")
    }

    fmt.Println("done")
}

The code returns errors found in the version with anchors and just returns done in the inlined version

openapi.zip