All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.sviperll.grumpinessy.MethodCallChainLineBreaksCheck Maven / Gradle / Ivy

/*
 * #%L
 * %%
 * Copyright (C) 2023 Victor Nazarov 
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

package com.github.sviperll.grumpinessy;

import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

public class MethodCallChainLineBreaksCheck extends AbstractCheck {

    @Override
    public int[] getDefaultTokens() {
        return new int[] {TokenTypes.METHOD_CALL};
    }

    @Override
    public int[] getAcceptableTokens() {
        return getDefaultTokens();
    }

    @Override
    public int[] getRequiredTokens() {
        return getDefaultTokens();
    }

    @Override
    public void visitToken(DetailAST ast) {
        List calls = new ArrayList<>();
        while (ast != null && isMethodCall(ast) && hasDot(ast) && calls.size() < 3) {
            calls.add(ast);
            ast = getTarget(ast);
        }
        if (calls.size() == 2 && getDotLine(calls.get(0)) != getDotLine(calls.get(1))) {
            DetailAST call = calls.get(1);
            DetailAST dot = call.findFirstToken(TokenTypes.DOT);
            DetailAST target = getTarget(call);
            CodeSpan span = getCodeSpan(target);
            if (isMultilineCall(call) && span.end().line() == dot.getLineNo()) {
                log(call, "line.break.is.required.complex.first.method.call.in.chain");
            }
        }
        if (calls.size() < 3)
            return;
        int[] callLines =
                calls.stream()
                        .mapToInt(MethodCallChainLineBreaksCheck::getDotLine)
                        .toArray();
        if (callLines[0] == callLines[1] && callLines[1] != callLines[2]) {
            log(calls.get(0), "multiple.method.calls.in.chain.on.same.line");
        }
        if (callLines[0] != callLines[1] && callLines[1] == callLines[2]) {
            log(calls.get(1), "multiple.method.calls.in.chain.on.same.line");
        }
    }

    private static CodeSpan getCodeSpan(DetailAST ast) {
        Location location = new Location(ast.getLineNo(), ast.getColumnNo());
        CodeSpan span = new CodeSpan(location, location);
        return Stream.iterate(ast.getFirstChild(), Objects::nonNull, DetailAST::getNextSibling)
                .map(MethodCallChainLineBreaksCheck::getCodeSpan)
                .reduce(span, CodeSpan::cover);
    }

    private boolean hasDot(DetailAST ast) {
        return ast.findFirstToken(TokenTypes.DOT) != null;
    }

    private static boolean isMethodCall(DetailAST ast) {
        return ast.getType() == TokenTypes.METHOD_CALL;
    }

    private static int getDotLine(DetailAST call) {
        return call.findFirstToken(TokenTypes.DOT).getLineNo();
    }

    private static DetailAST getTarget(DetailAST call) {
        DetailAST dot = call.findFirstToken(TokenTypes.DOT);
        return dot == null ? null : dot.getFirstChild();
    }

    private static boolean isMultilineCall(DetailAST call) {
        DetailAST dot = call.findFirstToken(TokenTypes.DOT);
        DetailAST rparen = call.findFirstToken(TokenTypes.RPAREN);
        return dot != null && rparen != null && dot.getLineNo() != rparen.getLineNo();
    }

    record Location(int line, int column) implements Comparable {
        private static final Comparator COMPARATOR =
                Comparator.comparingInt(Location::line).thenComparingInt(Location::column);
        static Location min(Location location1, Location location2) {
            return location1.compareTo(location2) <= 0 ? location1 : location2;
        }

        static Location max(Location location1, Location location2) {
            return location1.compareTo(location2) >= 0 ? location1 : location2;
        }

        @Override
        public int compareTo(Location that) {
            return COMPARATOR.compare(this, that);
        }
    }
    record CodeSpan(Location start, Location end) {
        static CodeSpan cover(CodeSpan span1, CodeSpan span2) {
            Location start = Location.min(span1.start(), span2.start());
            Location end = Location.max(span1.end(), span2.end());
            return new CodeSpan(start, end);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy