# Copyright (C) 2023 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Use this rule to report duplicated anchors and aliases referencing undeclared
anchors.

.. rubric:: Options

* Set ``forbid-undeclared-aliases`` to ``true`` to avoid aliases that reference
  an anchor that hasn't been declared (either not declared at all, or declared
  later in the document).
* Set ``forbid-duplicated-anchors`` to ``true`` to avoid duplications of a same
  anchor.
* Set ``forbid-unused-anchors`` to ``true`` to avoid anchors being declared but
  not used anywhere in the YAML document via alias.

.. rubric:: Default values (when enabled)

.. code-block:: yaml

 rules:
   anchors:
     forbid-undeclared-aliases: true
     forbid-duplicated-anchors: false
     forbid-unused-anchors: false

.. rubric:: Examples

#. With ``anchors: {forbid-undeclared-aliases: true}``

   the following code snippet would **PASS**:
   ::

    ---
    - &anchor
      foo: bar
    - *anchor

   the following code snippet would **FAIL**:
   ::

    ---
    - &anchor
      foo: bar
    - *unknown

   the following code snippet would **FAIL**:
   ::

    ---
    - &anchor
      foo: bar
    - <<: *unknown
      extra: value

#. With ``anchors: {forbid-duplicated-anchors: true}``

   the following code snippet would **PASS**:
   ::

    ---
    - &anchor1 Foo Bar
    - &anchor2 [item 1, item 2]

   the following code snippet would **FAIL**:
   ::

    ---
    - &anchor Foo Bar
    - &anchor [item 1, item 2]

#. With ``anchors: {forbid-unused-anchors: true}``

   the following code snippet would **PASS**:
   ::

    ---
    - &anchor
      foo: bar
    - *anchor

   the following code snippet would **FAIL**:
   ::

    ---
    - &anchor
      foo: bar
    - items:
      - item1
      - item2
"""


import yaml

from yamllint.linter import LintProblem


ID = 'anchors'
TYPE = 'token'
CONF = {'forbid-undeclared-aliases': bool,
        'forbid-duplicated-anchors': bool,
        'forbid-unused-anchors': bool}
DEFAULT = {'forbid-undeclared-aliases': True,
           'forbid-duplicated-anchors': False,
           'forbid-unused-anchors': False}


def check(conf, token, prev, next, nextnext, context):
    if (conf['forbid-undeclared-aliases'] or
            conf['forbid-duplicated-anchors'] or
            conf['forbid-unused-anchors']):
        if isinstance(token, (
                yaml.StreamStartToken,
                yaml.DocumentStartToken,
                yaml.DocumentEndToken)):
            context['anchors'] = {}

    if (conf['forbid-undeclared-aliases'] and
            isinstance(token, yaml.AliasToken) and
            token.value not in context['anchors']):
        yield LintProblem(
            token.start_mark.line + 1, token.start_mark.column + 1,
            f'found undeclared alias "{token.value}"')

    if (conf['forbid-duplicated-anchors'] and
            isinstance(token, yaml.AnchorToken) and
            token.value in context['anchors']):
        yield LintProblem(
            token.start_mark.line + 1, token.start_mark.column + 1,
            f'found duplicated anchor "{token.value}"')

    if conf['forbid-unused-anchors']:
        # Unused anchors can only be detected at the end of Document.
        # End of document can be either
        #   - end of stream
        #   - end of document sign '...'
        #   - start of a new document sign '---'
        # If next token indicates end of document,
        # check if the anchors have been used or not.
        # If they haven't been used, report problem on those anchors.
        if isinstance(next, (yaml.StreamEndToken,
                             yaml.DocumentStartToken,
                             yaml.DocumentEndToken)):
            for anchor, info in context['anchors'].items():
                if not info['used']:
                    yield LintProblem(info['line'] + 1,
                                      info['column'] + 1,
                                      f'found unused anchor "{anchor}"')
        elif isinstance(token, yaml.AliasToken):
            context['anchors'].get(token.value, {})['used'] = True

    if (conf['forbid-undeclared-aliases'] or
            conf['forbid-duplicated-anchors'] or
            conf['forbid-unused-anchors']):
        if isinstance(token, yaml.AnchorToken):
            context['anchors'][token.value] = {
                'line': token.start_mark.line,
                'column': token.start_mark.column,
                'used': False
            }
