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

com.tngtech.archunit.library.freeze.ViolationLineMatcherFactory Maven / Gradle / Ivy

/*
 * Copyright 2014-2023 TNG Technology Consulting GmbH
 *
 * 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.
 */
package com.tngtech.archunit.library.freeze;

import com.tngtech.archunit.ArchConfiguration;
import com.tngtech.archunit.base.MayResolveTypesViaReflection;

import static com.tngtech.archunit.base.ReflectionUtils.newInstanceOf;
import static java.lang.Character.isDigit;

class ViolationLineMatcherFactory {
    private static final String FREEZE_LINE_MATCHER_PROPERTY = "freeze.lineMatcher";
    private static final ViolationLineMatcher DEFAULT_MATCHER = new FuzzyViolationLineMatcher();

    static ViolationLineMatcher create() {
        return ArchConfiguration.get().containsProperty(FREEZE_LINE_MATCHER_PROPERTY)
                ? createInstance(ArchConfiguration.get().getProperty(FREEZE_LINE_MATCHER_PROPERTY))
                : DEFAULT_MATCHER;
    }

    @MayResolveTypesViaReflection(reason = "This is not part of the import process")
    private static ViolationLineMatcher createInstance(String lineMatcherClassName) {
        try {
            return (ViolationLineMatcher) newInstanceOf(Class.forName(lineMatcherClassName));
        } catch (Exception e) {
            String message = String.format("Could not instantiate %s of configured type '%s=%s'",
                    ViolationLineMatcher.class.getSimpleName(), FREEZE_LINE_MATCHER_PROPERTY, lineMatcherClassName);
            throw new ViolationLineMatcherInitializationFailedException(message, e);
        }
    }

    /**
     * ignores numbers that are potentially line numbers (digits following a ':' and preceding a ')')
     * or compiler-generated numbers of anonymous classes or lambda expressions (digits following a '$').
     */
    private static class FuzzyViolationLineMatcher implements ViolationLineMatcher {
        @Override
        public boolean matches(String str1, String str2) {
            // Compare relevant substrings, in a more performant way than a regex solution like this:
            //
            // normalize = str -> str.replaceAll(":\\d+\\)", ":0)").replaceAll("\\$\\d+", "\\$0");
            // return normalize.apply(str1).equals(normalize.apply(str2));

            RelevantPartIterator relevantPart1 = new RelevantPartIterator(str1);
            RelevantPartIterator relevantPart2 = new RelevantPartIterator(str2);
            while (relevantPart1.hasNext() && relevantPart2.hasNext()) {
                if (!relevantPart1.next().equals(relevantPart2.next())) {
                    return false;
                }
            }
            return !relevantPart1.hasNext() && !relevantPart2.hasNext();
        }

        static class RelevantPartIterator {
            private final String str;
            private final int length;
            // (start, end) is the (first, last) index of current relevant part, moved through string during iteration:
            private int start = 0;
            private int end = -1;

            RelevantPartIterator(String str) {
                this.str = str;
                this.length = str.length();
            }

            boolean hasNext() {
                if (start >= length) {
                    return false;
                }
                if (end >= 0) {
                    start = findStartIndexOfNextRelevantPart();
                }
                return start < length;
            }

            public String next() {
                end = Math.min(nextIndexOfCharacterOrEndOfString(':'), nextIndexOfCharacterOrEndOfString('$'));
                return str.substring(start, end + 1);
            }

            private int nextIndexOfCharacterOrEndOfString(char ch) {
                int i = str.indexOf(ch, start);
                return i >= 0 ? i : length - 1;
            }

            private int findStartIndexOfNextRelevantPart() {
                int startOfIgnoredPart = end + 1;
                int indexOfNonDigit = findIndexOfNextNonDigitChar(startOfIgnoredPart);
                if (str.charAt(end) == ':') {
                    boolean foundNumber = indexOfNonDigit > startOfIgnoredPart;
                    boolean foundClosingParenthesisAfterNumber = foundNumber && indexOfNonDigit < length && str.charAt(indexOfNonDigit) == ')';
                    return foundClosingParenthesisAfterNumber ? indexOfNonDigit + 1 : startOfIgnoredPart;
                } else {  // str.charAt(end) == '$'
                    return indexOfNonDigit;
                }
            }

            private int findIndexOfNextNonDigitChar(int index) {
                while (index < length && isDigit(str.charAt(index))) {
                    index++;
                }
                return index;
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy