Skip to main content

src/yum@yaml@diagnostic.erl

-module(yum@yaml@diagnostic).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/yum/yaml/diagnostic.gleam").
-export([collect/1, severity/1, is_error/1, has_errors/1, errors/1, warnings/1, message/1, span/1, related/1, related_message/1, related_span/1]).
-export_type([severity/0, related/0, diagnostic/0, seen_key/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Typed diagnostics produced while resolving YAML.\n"
    "\n"
    " Diagnostics describe semantic YAML issues such as duplicate mapping keys,\n"
    " unknown aliases, invalid directives, and invalid tags. Parse a document,\n"
    " resolve it with [`yum/yaml.resolve`](../yaml.html#resolve), then inspect the\n"
    " diagnostics kept on the resolved document.\n"
    "\n"
    " ```gleam\n"
    " import gleam/list\n"
    " import yum/yaml\n"
    " import yum/yaml/diagnostic\n"
    "\n"
    " pub fn example() {\n"
    "   let assert Ok(document) = yaml.parse(\"\n"
    " name: one\n"
    " name: two\n"
    " \")\n"
    "   let assert Ok(document) = yaml.resolve(document)\n"
    "\n"
    "   let messages =\n"
    "     document\n"
    "     |> yaml.diagnostics()\n"
    "     |> list.map(diagnostic.message)\n"
    "\n"
    "   assert messages == [\"Duplicate mapping key `name`\"]\n"
    " }\n"
    " ```\n"
).

-type severity() :: warning | diagnostic_error.

-type related() :: {first_mapping_key, yum@yaml@node:span()} |
    {first_anchor, yum@yaml@node:span()} |
    {first_yaml_directive, yum@yaml@node:span()}.

-type diagnostic() :: {duplicate_mapping_key,
        binary(),
        yum@yaml@node:span(),
        yum@yaml@node:span()} |
    {duplicate_anchor, binary(), yum@yaml@node:span(), yum@yaml@node:span()} |
    {unknown_alias, binary(), yum@yaml@node:span()} |
    {invalid_tag_directive, yum@yaml@node:span()} |
    {invalid_yaml_directive, yum@yaml@node:span()} |
    {unsupported_yaml_version, binary(), yum@yaml@node:span()} |
    {duplicate_yaml_directive, yum@yaml@node:span(), yum@yaml@node:span()} |
    {unknown_tag_handle, binary(), yum@yaml@node:span()} |
    {invalid_tag, binary(), yum@yaml@node:span()} |
    {invalid_merge_target, yum@yaml@node:kind_name(), yum@yaml@node:span()}.

-type seen_key() :: {seen_key, binary(), yum@yaml@node:span()}.

-file("src/yum/yaml/diagnostic.gleam", 387).
-spec bool_identity(boolean()) -> binary().
bool_identity(Value) ->
    case Value of
        true ->
            <<"true"/utf8>>;

        false ->
            <<"false"/utf8>>
    end.

-file("src/yum/yaml/diagnostic.gleam", 373).
-spec key_label(yum@yaml@node:node_()) -> binary().
key_label(Key) ->
    case yum@yaml@node:kind(Key) of
        null ->
            <<"null"/utf8>>;

        {bool, Value} ->
            bool_identity(Value);

        {int, Value@1} ->
            erlang:integer_to_binary(Value@1);

        {float, Value@2} ->
            gleam_stdlib:float_to_string(Value@2);

        pos_inf ->
            <<".inf"/utf8>>;

        neg_inf ->
            <<"-.inf"/utf8>>;

        nan ->
            <<".nan"/utf8>>;

        {string, Value@3} ->
            Value@3;

        {sequence, _} ->
            <<"<complex key>"/utf8>>;

        {mapping, _} ->
            <<"<complex key>"/utf8>>
    end.

-file("src/yum/yaml/diagnostic.gleam", 233).
-spec duplicate_key_diagnostic(yum@yaml@node:node_(), seen_key()) -> diagnostic().
duplicate_key_diagnostic(Duplicate, First) ->
    {duplicate_mapping_key,
        erlang:element(2, First),
        yum@yaml@node:span(Duplicate),
        erlang:element(3, First)}.

-file("src/yum/yaml/diagnostic.gleam", 359).
-spec key_identity(yum@yaml@node:node_()) -> gleam@option:option(binary()).
key_identity(Key) ->
    case yum@yaml@node:kind(Key) of
        null ->
            {some, <<"null:"/utf8>>};

        {bool, Value} ->
            {some, <<"bool:"/utf8, (bool_identity(Value))/binary>>};

        {int, Value@1} ->
            {some, <<"int:"/utf8, (erlang:integer_to_binary(Value@1))/binary>>};

        {float, Value@2} ->
            {some,
                <<"float:"/utf8,
                    (gleam_stdlib:float_to_string(Value@2))/binary>>};

        pos_inf ->
            {some, <<"float:.inf"/utf8>>};

        neg_inf ->
            {some, <<"float:-.inf"/utf8>>};

        nan ->
            {some, <<"float:.nan"/utf8>>};

        {string, Value@3} ->
            {some, <<"string:"/utf8, Value@3/binary>>};

        {sequence, _} ->
            none;

        {mapping, _} ->
            none
    end.

-file("src/yum/yaml/diagnostic.gleam", 204).
-spec duplicate_key_diagnostics(
    list({yum@yaml@node:node_(), yum@yaml@node:node_()})
) -> list(diagnostic()).
duplicate_key_diagnostics(Entries) ->
    {_, Diagnostics@1} = gleam@list:fold(
        Entries,
        {maps:new(), []},
        fun(Acc, Entry) ->
            {Seen, Diagnostics} = Acc,
            {Key, _} = Entry,
            case key_identity(Key) of
                none ->
                    Acc;

                {some, Identity} ->
                    case gleam_stdlib:map_get(Seen, Identity) of
                        {ok, First} ->
                            {Seen,
                                [duplicate_key_diagnostic(Key, First) |
                                    Diagnostics]};

                        {error, _} ->
                            {gleam@dict:insert(
                                    Seen,
                                    Identity,
                                    {seen_key,
                                        key_label(Key),
                                        yum@yaml@node:span(Key)}
                                ),
                                Diagnostics}
                    end
            end
        end
    ),
    lists:reverse(Diagnostics@1).

-file("src/yum/yaml/diagnostic.gleam", 132).
-spec collect_node_properties(
    yum@yaml@node:node_(),
    gleam@dict:dict(binary(), yum@yaml@node:node_())
) -> {gleam@dict:dict(binary(), yum@yaml@node:node_()), list(diagnostic())}.
collect_node_properties(Value, Anchors) ->
    Alias_diagnostics = case yum@yaml@node:alias(Value) of
        {some, Alias} ->
            case gleam@dict:has_key(Anchors, Alias) of
                true ->
                    [];

                false ->
                    [{unknown_alias, Alias, yum@yaml@node:span(Value)}]
            end;

        none ->
            []
    end,
    {Anchors@1, Anchor_diagnostics} = case yum@yaml@node:anchor(Value) of
        {some, Anchor} ->
            Diagnostics = case gleam_stdlib:map_get(Anchors, Anchor) of
                {ok, Original} ->
                    [{duplicate_anchor,
                            Anchor,
                            yum@yaml@node:span(Value),
                            yum@yaml@node:span(Original)}];

                {error, _} ->
                    []
            end,
            {gleam@dict:insert(Anchors, Anchor, Value), Diagnostics};

        none ->
            {Anchors, []}
    end,
    {Anchors@1, lists:append(Alias_diagnostics, Anchor_diagnostics)}.

-file("src/yum/yaml/diagnostic.gleam", 192).
-spec collect_sequence_entries(
    list(yum@yaml@node:node_()),
    gleam@dict:dict(binary(), yum@yaml@node:node_())
) -> {gleam@dict:dict(binary(), yum@yaml@node:node_()), list(diagnostic())}.
collect_sequence_entries(Entries, Anchors) ->
    gleam@list:fold(
        Entries,
        {Anchors, []},
        fun(Acc, Entry) ->
            {Anchors@1, Diagnostics} = Acc,
            {Anchors@2, Entry_diagnostics} = collect_with_anchors(
                Entry,
                Anchors@1
            ),
            {Anchors@2, lists:append(Diagnostics, Entry_diagnostics)}
        end
    ).

-file("src/yum/yaml/diagnostic.gleam", 166).
-spec collect_mapping_entries(
    list({yum@yaml@node:node_(), yum@yaml@node:node_()}),
    gleam@dict:dict(binary(), yum@yaml@node:node_())
) -> {gleam@dict:dict(binary(), yum@yaml@node:node_()), list(diagnostic())}.
collect_mapping_entries(Entries, Anchors) ->
    {Anchors@4, Nested_diagnostics} = gleam@list:fold(
        Entries,
        {Anchors, []},
        fun(Acc, Entry) ->
            {Anchors@1, Diagnostics} = Acc,
            {Key, Value} = Entry,
            {Anchors@2, Key_diagnostics} = collect_with_anchors(Key, Anchors@1),
            {Anchors@3, Value_diagnostics} = collect_with_anchors(
                Value,
                Anchors@2
            ),
            {Anchors@3,
                lists:append(
                    Diagnostics,
                    lists:append(Key_diagnostics, Value_diagnostics)
                )}
        end
    ),
    {Anchors@4,
        lists:append(duplicate_key_diagnostics(Entries), Nested_diagnostics)}.

-file("src/yum/yaml/diagnostic.gleam", 115).
-spec collect_with_anchors(
    yum@yaml@node:node_(),
    gleam@dict:dict(binary(), yum@yaml@node:node_())
) -> {gleam@dict:dict(binary(), yum@yaml@node:node_()), list(diagnostic())}.
collect_with_anchors(Value, Anchors) ->
    {Anchors@1, Property_diagnostics} = collect_node_properties(Value, Anchors),
    {Anchors@2, Nested_diagnostics} = case yum@yaml@node:kind(Value) of
        {mapping, Entries} ->
            collect_mapping_entries(Entries, Anchors@1);

        {sequence, Entries@1} ->
            collect_sequence_entries(Entries@1, Anchors@1);

        _ ->
            {Anchors@1, []}
    end,
    {Anchors@2, lists:append(Property_diagnostics, Nested_diagnostics)}.

-file("src/yum/yaml/diagnostic.gleam", 110).
?DOC(" Collects non-fatal diagnostics for a parsed YAML node tree.\n").
-spec collect(yum@yaml@node:node_()) -> list(diagnostic()).
collect(Value) ->
    {_, Diagnostics} = collect_with_anchors(Value, maps:new()),
    Diagnostics.

-file("src/yum/yaml/diagnostic.gleam", 243).
?DOC(" Returns the severity for a diagnostic variant.\n").
-spec severity(diagnostic()) -> severity().
severity(Diagnostic) ->
    case Diagnostic of
        {duplicate_mapping_key, _, _, _} ->
            warning;

        {duplicate_anchor, _, _, _} ->
            warning;

        {unknown_alias, _, _} ->
            diagnostic_error;

        {invalid_tag_directive, _} ->
            diagnostic_error;

        {invalid_yaml_directive, _} ->
            diagnostic_error;

        {unsupported_yaml_version, _, _} ->
            diagnostic_error;

        {duplicate_yaml_directive, _, _} ->
            diagnostic_error;

        {unknown_tag_handle, _, _} ->
            diagnostic_error;

        {invalid_tag, _, _} ->
            diagnostic_error;

        {invalid_merge_target, _, _} ->
            diagnostic_error
    end.

-file("src/yum/yaml/diagnostic.gleam", 260).
?DOC(" Returns True when a diagnostic is fatal.\n").
-spec is_error(diagnostic()) -> boolean().
is_error(Diagnostic) ->
    severity(Diagnostic) =:= diagnostic_error.

-file("src/yum/yaml/diagnostic.gleam", 266).
?DOC(" Returns True when any diagnostic is fatal.\n").
-spec has_errors(list(diagnostic())) -> boolean().
has_errors(Diagnostics) ->
    _pipe = Diagnostics,
    gleam@list:any(_pipe, fun is_error/1).

-file("src/yum/yaml/diagnostic.gleam", 273).
?DOC(" Keeps only fatal diagnostics.\n").
-spec errors(list(diagnostic())) -> list(diagnostic()).
errors(Diagnostics) ->
    _pipe = Diagnostics,
    gleam@list:filter(_pipe, fun is_error/1).

-file("src/yum/yaml/diagnostic.gleam", 280).
?DOC(" Keeps only warning diagnostics.\n").
-spec warnings(list(diagnostic())) -> list(diagnostic()).
warnings(Diagnostics) ->
    _pipe = Diagnostics,
    gleam@list:filter(_pipe, fun(Diagnostic) -> not is_error(Diagnostic) end).

-file("src/yum/yaml/diagnostic.gleam", 287).
?DOC(" Renders a human-readable diagnostic message.\n").
-spec message(diagnostic()) -> binary().
message(Diagnostic) ->
    case Diagnostic of
        {duplicate_mapping_key, Key, _, _} ->
            <<<<"Duplicate mapping key `"/utf8, Key/binary>>/binary, "`"/utf8>>;

        {duplicate_anchor, Anchor, _, _} ->
            <<<<"Duplicate anchor `"/utf8, Anchor/binary>>/binary, "`"/utf8>>;

        {unknown_alias, Alias, _} ->
            <<<<"Unknown alias `"/utf8, Alias/binary>>/binary, "`"/utf8>>;

        {invalid_tag_directive, _} ->
            <<"Invalid %TAG directive"/utf8>>;

        {invalid_yaml_directive, _} ->
            <<"Invalid %YAML directive"/utf8>>;

        {unsupported_yaml_version, Version, _} ->
            <<<<"Unsupported YAML version `"/utf8, Version/binary>>/binary,
                "`"/utf8>>;

        {duplicate_yaml_directive, _, _} ->
            <<"Duplicate %YAML directive"/utf8>>;

        {unknown_tag_handle, Handle, _} ->
            <<<<"Unknown tag handle `"/utf8, Handle/binary>>/binary, "`"/utf8>>;

        {invalid_tag, Tag, _} ->
            <<<<"Invalid tag `"/utf8, Tag/binary>>/binary, "`"/utf8>>;

        {invalid_merge_target, _, _} ->
            <<"Merge key must reference a mapping"/utf8>>
    end.

-file("src/yum/yaml/diagnostic.gleam", 305).
?DOC(" Returns the primary source span for a diagnostic.\n").
-spec span(diagnostic()) -> yum@yaml@node:span().
span(Diagnostic) ->
    case Diagnostic of
        {duplicate_mapping_key, _, Duplicate, _} ->
            Duplicate;

        {duplicate_anchor, _, Duplicate@1, _} ->
            Duplicate@1;

        {unknown_alias, _, Span} ->
            Span;

        {invalid_tag_directive, Span@1} ->
            Span@1;

        {invalid_yaml_directive, Span@2} ->
            Span@2;

        {unsupported_yaml_version, _, Span@3} ->
            Span@3;

        {duplicate_yaml_directive, Duplicate@2, _} ->
            Duplicate@2;

        {unknown_tag_handle, _, Span@4} ->
            Span@4;

        {invalid_tag, _, Span@5} ->
            Span@5;

        {invalid_merge_target, _, Span@6} ->
            Span@6
    end.

-file("src/yum/yaml/diagnostic.gleam", 322).
?DOC(" Returns related source locations for a diagnostic.\n").
-spec related(diagnostic()) -> list(related()).
related(Diagnostic) ->
    case Diagnostic of
        {duplicate_mapping_key, _, _, Original} ->
            [{first_mapping_key, Original}];

        {duplicate_anchor, _, _, Original@1} ->
            [{first_anchor, Original@1}];

        {duplicate_yaml_directive, _, Original@2} ->
            [{first_yaml_directive, Original@2}];

        {unknown_alias, _, _} ->
            [];

        {invalid_tag_directive, _} ->
            [];

        {invalid_yaml_directive, _} ->
            [];

        {unsupported_yaml_version, _, _} ->
            [];

        {unknown_tag_handle, _, _} ->
            [];

        {invalid_tag, _, _} ->
            [];

        {invalid_merge_target, _, _} ->
            []
    end.

-file("src/yum/yaml/diagnostic.gleam", 341).
?DOC(" Renders a human-readable related-location message.\n").
-spec related_message(related()) -> binary().
related_message(Related) ->
    case Related of
        {first_mapping_key, _} ->
            <<"First key appears here"/utf8>>;

        {first_anchor, _} ->
            <<"First anchor appears here"/utf8>>;

        {first_yaml_directive, _} ->
            <<"First %YAML directive appears here"/utf8>>
    end.

-file("src/yum/yaml/diagnostic.gleam", 351).
?DOC(" Returns the source span for a related location.\n").
-spec related_span(related()) -> yum@yaml@node:span().
related_span(Related) ->
    case Related of
        {first_mapping_key, Span} ->
            Span;

        {first_anchor, Span@1} ->
            Span@1;

        {first_yaml_directive, Span@2} ->
            Span@2
    end.