NoOrphanAliasInBoundariesListener.java

package org.thewonderlemming.c4plantuml.linter.rules.builtin;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTreeListener;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.thewonderlemming.c4plantuml.commons.Reporter;
import org.thewonderlemming.c4plantuml.grammars.C4BaseListener;
import org.thewonderlemming.c4plantuml.grammars.SourceType;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L1Parser;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L2Parser;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L3Parser;

/**
 * An ANTLR 4 {@link ParseTreeListener} implementation that verifies that aliases in use in a C4 boundaries are all
 * referring to a valid entity.
 *
 * @author thewonderlemming
 *
 */
public class NoOrphanAliasInBoundariesListener extends C4BaseListener {

    private class UsedAlias {

        public final String alias;

        public final ParserRuleContext context;


        public UsedAlias(final TerminalNode alias, final ParserRuleContext context) {

            this.alias = alias.getText();
            this.context = context;
        }
    }


    private static final String MESSAGE_FORMAT = "Alias <%s> is used in the boundary <'%s' -> '%s': '%s'> without being properly declared";

    private final Set<String> declaredAliases = new HashSet<>();

    private final Reporter reporter;

    private final NoOrphanAliasInBoundariesRule rule;

    private final List<UsedAlias> usedAliases = new ArrayList<>();


    /**
     * Default constructor.
     *
     * @param rule the {@link NoOrphanAliasInBoundariesRule} instance to refer to.
     * @param reporter {@link Reporter} instance to report to.
     */
    public NoOrphanAliasInBoundariesListener(final NoOrphanAliasInBoundariesRule rule, final Reporter reporter) {

        this.rule = rule;
        this.reporter = reporter;
    }

    /**
     * Verifies that the aliases that were collected in boundaries were all declared in an entity, or report them.
     * Cleans the aliases list once done.
     */
    public void checkThatNoAliasIsOrphanThenClearCollected() {

        this.usedAliases.forEach(usedAlias -> {

            if (!this.declaredAliases.contains(usedAlias.alias)) {

                final String message = buildErrorMessageFromContext(usedAlias);
                this.reporter.report(message);
            }
        });

        this.usedAliases.clear();
        this.declaredAliases.clear();
    }

    /**
     * Collects the aliases of a boundary in a {@link SourceType#C4_L1} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterBoundary(final C4L1Parser.BoundaryContext ctx) {
        ctx.Alias().forEach(alias -> collectUsedAlias(alias, ctx));
    }

    /**
     * Collects the aliases of a boundary in a {@link SourceType#C4_L1} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterBoundary(final C4L2Parser.BoundaryContext ctx) {
        ctx.Alias().forEach(alias -> collectUsedAlias(alias, ctx));
    }

    /**
     * Collects the aliases of a boundary in a {@link SourceType#C4_L1} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterBoundary(final C4L3Parser.BoundaryContext ctx) {
        ctx.Alias().forEach(alias -> collectUsedAlias(alias, ctx));
    }

    /**
     * Collects the alias of a cloud definition in a {@link SourceType#C4_L1} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterCloud(final C4L1Parser.CloudContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of a cloud definition in a {@link SourceType#C4_L2} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterCloud(final C4L2Parser.CloudContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of a cloud definition in a {@link SourceType#C4_L3} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterCloud(final C4L3Parser.CloudContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of a component definition in a {@link SourceType#C4_L3} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterComponent(final C4L3Parser.ComponentContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of a container definition in a {@link SourceType#C4_L2} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterContainer(final C4L2Parser.ContainerContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of a container definition in a {@link SourceType#C4_L3} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterContainer(final C4L3Parser.ContainerContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the aliases of a container boundary definition in a {@link SourceType#C4_L3} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterContainer_boundary(final C4L3Parser.Container_boundaryContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the aliases of an enterprise boundary definition in a {@link SourceType#C4_L1} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterEnterprise_boundary(final C4L1Parser.Enterprise_boundaryContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the aliases of an enterprise boundary definition in a {@link SourceType#C4_L2} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterEnterprise_boundary(final C4L2Parser.Enterprise_boundaryContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the aliases of an enterprise boundary definition in a {@link SourceType#C4_L3} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterEnterprise_boundary(final C4L3Parser.Enterprise_boundaryContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of a person definition in a {@link SourceType#C4_L1} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterPerson(final C4L1Parser.PersonContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of a person definition in a {@link SourceType#C4_L2} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterPerson(final C4L2Parser.PersonContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of a person definition in a {@link SourceType#C4_L3} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterPerson(final C4L3Parser.PersonContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of an external person definition in a {@link SourceType#C4_L1} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterPerson_ext(final C4L1Parser.Person_extContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of an external person definition in a {@link SourceType#C4_L2} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterPerson_ext(final C4L2Parser.Person_extContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of an external person definition in a {@link SourceType#C4_L3} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterPerson_ext(final C4L3Parser.Person_extContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of a system definition in a {@link SourceType#C4_L1} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterSystem(final C4L1Parser.SystemContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of a system definition in a {@link SourceType#C4_L2} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterSystem(final C4L2Parser.SystemContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of a system definition in a {@link SourceType#C4_L3} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterSystem(final C4L3Parser.SystemContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of a system boundary definition in a {@link SourceType#C4_L2} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterSystem_boundary(final C4L2Parser.System_boundaryContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of an external system definition in a {@link SourceType#C4_L1} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterSystem_ext(final C4L1Parser.System_extContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of an external system definition in a {@link SourceType#C4_L2} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterSystem_ext(final C4L2Parser.System_extContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    /**
     * Collects the alias of an external system definition in a {@link SourceType#C4_L3} grammar.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public void enterSystem_ext(final C4L3Parser.System_extContext ctx) {
        collectDeclaredAlias(ctx.Alias());
    }

    private String buildErrorMessageFromC4L1Context(final UsedAlias usedAlias) {

        final C4L1Parser.BoundaryContext ctx = ((C4L1Parser.BoundaryContext) usedAlias.context);
        final String sourceAlias = ctx.Alias(0).getText();
        final String targetAlias = ctx.Alias(1).getText();
        final String description = ctx.String().getText();

        return String.format(MESSAGE_FORMAT, usedAlias.alias, sourceAlias, targetAlias, description);
    }

    private String buildErrorMessageFromC4L2Context(final UsedAlias usedAlias) {

        final C4L2Parser.BoundaryContext ctx = ((C4L2Parser.BoundaryContext) usedAlias.context);
        final String sourceAlias = ctx.Alias(0).getText();
        final String targetAlias = ctx.Alias(1).getText();
        final String description = ctx.String().getText();

        return String.format(MESSAGE_FORMAT, usedAlias.alias, sourceAlias, targetAlias, description);
    }

    private String buildErrorMessageFromC4L3Context(final UsedAlias usedAlias) {

        final C4L3Parser.BoundaryContext ctx = ((C4L3Parser.BoundaryContext) usedAlias.context);
        final String sourceAlias = ctx.Alias(0).getText();
        final String targetAlias = ctx.Alias(1).getText();
        final String description = ctx.String().getText();

        return String.format(MESSAGE_FORMAT, usedAlias.alias, sourceAlias, targetAlias, description);
    }

    private String buildErrorMessageFromContext(final UsedAlias usedAlias) {

        if (usedAlias.context instanceof C4L1Parser.BoundaryContext) {
            return buildErrorMessageFromC4L1Context(usedAlias);

        } else if (usedAlias.context instanceof C4L2Parser.BoundaryContext) {
            return buildErrorMessageFromC4L2Context(usedAlias);

        } else if (usedAlias.context instanceof C4L3Parser.BoundaryContext) {
            return buildErrorMessageFromC4L3Context(usedAlias);

        }

        this.rule
            .getLogger()
                .warn(
                    "The rule was unable to retrieve boundary information. Maybe the current source file is malformed?");

        return String.format(MESSAGE_FORMAT, usedAlias.alias, "?", "?", "?");
    }

    private void collectDeclaredAlias(final TerminalNode alias) {
        this.declaredAliases.add(alias.getText());
    }

    private void collectUsedAlias(final TerminalNode alias, final ParserRuleContext context) {

        final UsedAlias used = new UsedAlias(alias, context);
        this.usedAliases.add(used);
    }
}